Compare commits
413 Commits
phillip/ad
...
phillip/gu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39d41ace6 | ||
|
|
fe63436a57 | ||
|
|
5b93b9b37a | ||
|
|
1d55027791 | ||
|
|
83f0984da1 | ||
|
|
53d4f75cab | ||
|
|
da45eb2adf | ||
|
|
3bf4af8d8b | ||
|
|
f030691fac | ||
|
|
1f94e51693 | ||
|
|
86e7d7a72b | ||
|
|
140b852e03 | ||
|
|
8f949ce1cc | ||
|
|
5e21285370 | ||
|
|
7a65bc2d8d | ||
|
|
a32fadbcbd | ||
|
|
305192ed1f | ||
|
|
7644e202c9 | ||
|
|
d11c8442ef | ||
|
|
d8b5391425 | ||
|
|
dd287cd719 | ||
|
|
e809d1f6e4 | ||
|
|
da90fa6aaf | ||
|
|
77392db25a | ||
|
|
1bc1bf0621 | ||
|
|
635a258d62 | ||
|
|
384fb505c1 | ||
|
|
3e0bc36373 | ||
|
|
0a431afaaf | ||
|
|
8c911bcd41 | ||
|
|
dcb7ac5955 | ||
|
|
fb730942a0 | ||
|
|
9c92bf73f5 | ||
|
|
58f195fdb7 | ||
|
|
4b86c9c8a7 | ||
|
|
4cc689ec63 | ||
|
|
8690484f5e | ||
|
|
1f3e5b7a76 | ||
|
|
61c790f291 | ||
|
|
b3440fa3a8 | ||
|
|
a3f1835d1d | ||
|
|
9226f6f70e | ||
|
|
1130f9957f | ||
|
|
ad1fd03aad | ||
|
|
6c93033ad2 | ||
|
|
dd97b11b60 | ||
|
|
59ba07d4f3 | ||
|
|
d2bfd1e3a9 | ||
|
|
a8264bf526 | ||
|
|
f202f2b3d3 | ||
|
|
4ea9f8282e | ||
|
|
205d84a111 | ||
|
|
5810853cc2 | ||
|
|
4547204bd8 | ||
|
|
f17a0c91a3 | ||
|
|
16e1523b08 | ||
|
|
0f06ec1ab8 | ||
|
|
641266122a | ||
|
|
5ba939ee9c | ||
|
|
c979e568f1 | ||
|
|
93f0c240f9 | ||
|
|
ad04b077a4 | ||
|
|
7ffc454320 | ||
|
|
dae0fbff16 | ||
|
|
5648092112 | ||
|
|
275b15b773 | ||
|
|
1025635e34 | ||
|
|
836cbdb81e | ||
|
|
be922de7ba | ||
|
|
3a2f5e724d | ||
|
|
8a105c6a14 | ||
|
|
7f1c64a50e | ||
|
|
125f472f34 | ||
|
|
bafd273475 | ||
|
|
365cb1c2eb | ||
|
|
876d5a67d6 | ||
|
|
3078af8f2a | ||
|
|
dad1440138 | ||
|
|
12773d539e | ||
|
|
ae4130b108 | ||
|
|
ad0614282e | ||
|
|
5a7704aed7 | ||
|
|
2feadd6125 | ||
|
|
efe0b3cd9e | ||
|
|
96731da380 | ||
|
|
0c5dd5d8b5 | ||
|
|
2f943a22e6 | ||
|
|
666184d7e4 | ||
|
|
17d22dda3f | ||
|
|
d1a18c121d | ||
|
|
836d7f3991 | ||
|
|
ace9c3c46a | ||
|
|
068640311e | ||
|
|
f26d2a59ae | ||
|
|
03c7e9172e | ||
|
|
6fdc072ec3 | ||
|
|
e68661c04b | ||
|
|
4f567592ea | ||
|
|
63c9b7a894 | ||
|
|
eaec39188e | ||
|
|
ba6940eb81 | ||
|
|
f8a3e4d673 | ||
|
|
2727da6f6c | ||
|
|
fa97852e38 | ||
|
|
2c7da25a25 | ||
|
|
9a072e3e76 | ||
|
|
823b339d27 | ||
|
|
fe98d9485d | ||
|
|
407e1bb560 | ||
|
|
98a6535dc3 | ||
|
|
9948e8ee44 | ||
|
|
bce07ec357 | ||
|
|
836807aa1e | ||
|
|
ebbcbef6d5 | ||
|
|
ccc6c9867f | ||
|
|
20d31ed8c8 | ||
|
|
39ff6cbe05 | ||
|
|
1bf2efa885 | ||
|
|
4b45a6389c | ||
|
|
5ba7d2395e | ||
|
|
972f23e235 | ||
|
|
9f599b0c8e | ||
|
|
b937c2df0b | ||
|
|
9c4396027a | ||
|
|
2bab20d032 | ||
|
|
cb2ee670e3 | ||
|
|
b65d23d535 | ||
|
|
007cdf0ca2 | ||
|
|
1e4799bac6 | ||
|
|
47222445ad | ||
|
|
126b382da1 | ||
|
|
ec78831a81 | ||
|
|
9bfb2afd9c | ||
|
|
389124b83f | ||
|
|
eb25330296 | ||
|
|
29892ff5e3 | ||
|
|
99a31b322a | ||
|
|
1884c6c751 | ||
|
|
9456477953 | ||
|
|
e3512a2bdd | ||
|
|
6ce3f84458 | ||
|
|
484c3cbac8 | ||
|
|
c199beaf8c | ||
|
|
553aa01c25 | ||
|
|
8d1b10e458 | ||
|
|
0eaee9b1e4 | ||
|
|
41bbc475ab | ||
|
|
d6e03c765e | ||
|
|
dd6503d5ef | ||
|
|
36e5f39d7c | ||
|
|
d48e4a664f | ||
|
|
661b30e807 | ||
|
|
026e819271 | ||
|
|
1fab19acf4 | ||
|
|
5743fb86b0 | ||
|
|
5443bf2459 | ||
|
|
c0d5566417 | ||
|
|
ded71b46c5 | ||
|
|
9693ad321c | ||
|
|
dd3679f329 | ||
|
|
f3029953dc | ||
|
|
01881b2fd8 | ||
|
|
11a22d0f5d | ||
|
|
5f9bf07045 | ||
|
|
719c03e2f5 | ||
|
|
379afa9554 | ||
|
|
dbc23e89b8 | ||
|
|
0c6e254742 | ||
|
|
8327e69bdd | ||
|
|
2d953f4f59 | ||
|
|
7118d63949 | ||
|
|
20af8d038e | ||
|
|
3d9dfbb5e1 | ||
|
|
ae0b966f45 | ||
|
|
cef8a34c06 | ||
|
|
6432823eec | ||
|
|
563b780d85 | ||
|
|
aa9b1b2cac | ||
|
|
401e541b86 | ||
|
|
c13bed3bad | ||
|
|
b3c4817fb4 | ||
|
|
7c9c45ac5f | ||
|
|
95142e3684 | ||
|
|
dc1cce6ddb | ||
|
|
43cf77f33c | ||
|
|
93780d7056 | ||
|
|
2ad17d408e | ||
|
|
b0f7567367 | ||
|
|
3f2b1d3f79 | ||
|
|
29eb8ca10b | ||
|
|
8c71ca12b8 | ||
|
|
72a753626f | ||
|
|
35ebb12bf2 | ||
|
|
1ff418f62d | ||
|
|
e1aa437ea5 | ||
|
|
2a4239bf3c | ||
|
|
399563435b | ||
|
|
59f7e25c85 | ||
|
|
ad845dff43 | ||
|
|
fd1eb2d900 | ||
|
|
26cb6df9d9 | ||
|
|
b0aafb079a | ||
|
|
58f0837c50 | ||
|
|
a6378b3d43 | ||
|
|
ddbf95da92 | ||
|
|
4d31e0286b | ||
|
|
7a74825121 | ||
|
|
be0e8779d5 | ||
|
|
fffbe17bcc | ||
|
|
ca4ee8b513 | ||
|
|
30f1820a49 | ||
|
|
3bb6c391af | ||
|
|
a0383c785a | ||
|
|
99790c05f4 | ||
|
|
fc5fec9bfe | ||
|
|
9db5d4116d | ||
|
|
6676e94ef6 | ||
|
|
723adceb25 | ||
|
|
440d06da4a | ||
|
|
0ea84668a8 | ||
|
|
5893d8b9bb | ||
|
|
2c799b9c07 | ||
|
|
1550d9b4ee | ||
|
|
ade812b86d | ||
|
|
62e6fbef61 | ||
|
|
67a0f8b65a | ||
|
|
aa432022d3 | ||
|
|
86fb3c1fd1 | ||
|
|
ff2b4add8b | ||
|
|
4ba73dfbec | ||
|
|
e675ea9bd1 | ||
|
|
9c27d86ced | ||
|
|
58ee81adfc | ||
|
|
32c9904a6e | ||
|
|
b86e0a1549 | ||
|
|
154ac9bb38 | ||
|
|
a97060445a | ||
|
|
26b59de1de | ||
|
|
21c8b00ef6 | ||
|
|
c25b7293bb | ||
|
|
15e078cb34 | ||
|
|
f7bb17202b | ||
|
|
213b7696c5 | ||
|
|
fe5c95316b | ||
|
|
54617f8583 | ||
|
|
75c9731ca4 | ||
|
|
31afc45744 | ||
|
|
f6466b161b | ||
|
|
a36114e904 | ||
|
|
529f856ab9 | ||
|
|
9077e66973 | ||
|
|
a47a96b70d | ||
|
|
8a94e88786 | ||
|
|
b3aa236d3d | ||
|
|
4dd58ad89e | ||
|
|
317f7ab598 | ||
|
|
d6c47e7e81 | ||
|
|
1ed61a3d3d | ||
|
|
5c734cfa00 | ||
|
|
07f485a654 | ||
|
|
ae76271469 | ||
|
|
c8a8ecbe1f | ||
|
|
fbf69a4a34 | ||
|
|
7f38ffe676 | ||
|
|
a0e0c392e9 | ||
|
|
573e472077 | ||
|
|
955d22278d | ||
|
|
171ee93108 | ||
|
|
5fb0560f0b | ||
|
|
88b616e206 | ||
|
|
08829425cb | ||
|
|
1dbd2bf0dc | ||
|
|
157f98b331 | ||
|
|
3d689837d6 | ||
|
|
2b76bbe0db | ||
|
|
e75db79b50 | ||
|
|
60919671ea | ||
|
|
bca21c1cf0 | ||
|
|
f1993db0fa | ||
|
|
7351c16578 | ||
|
|
5bc8f5dd64 | ||
|
|
20517cd0b2 | ||
|
|
9a4081c54b | ||
|
|
97e0b31a3d | ||
|
|
af17930314 | ||
|
|
094b19f289 | ||
|
|
8e54cef68b | ||
|
|
1df8d5832f | ||
|
|
0542008b7f | ||
|
|
ffa89202e6 | ||
|
|
1203cbbad8 | ||
|
|
f9fb463128 | ||
|
|
ea398f6294 | ||
|
|
5f41042826 | ||
|
|
486b7d4da1 | ||
|
|
91b47e56ff | ||
|
|
9934e59629 | ||
|
|
50cc66d51c | ||
|
|
936c9dc4f3 | ||
|
|
946ade5da1 | ||
|
|
80068a3674 | ||
|
|
d7c9a7874b | ||
|
|
768e5b3f5b | ||
|
|
f3320d9ae3 | ||
|
|
d4538b0909 | ||
|
|
676ee74f19 | ||
|
|
9059f227fa | ||
|
|
6a14d0f3f3 | ||
|
|
3e5c623125 | ||
|
|
e559fb7e4b | ||
|
|
88a1cfb689 | ||
|
|
f12c4e75e6 | ||
|
|
90f08c58cd | ||
|
|
f6aa96c64c | ||
|
|
2b04a1b50c | ||
|
|
7297fb5241 | ||
|
|
98c5a68a8c | ||
|
|
8e643747f8 | ||
|
|
2483e19bee | ||
|
|
f9d3c6ed48 | ||
|
|
09a0e75351 | ||
|
|
644edc5b76 | ||
|
|
a64b994376 | ||
|
|
fb626ebf7e | ||
|
|
dd334f487e | ||
|
|
cd5c86fb69 | ||
|
|
7878761b6f | ||
|
|
d3b63abdd3 | ||
|
|
23fad37205 | ||
|
|
88558e6b98 | ||
|
|
a84ee8497b | ||
|
|
d560ee2da1 | ||
|
|
fd3fce110e | ||
|
|
1bce2b0e28 | ||
|
|
06a59bfe03 | ||
|
|
83a430afad | ||
|
|
949f638b6e | ||
|
|
2b2193e9ce | ||
|
|
0709bada87 | ||
|
|
506586b74c | ||
|
|
99b2ee273f | ||
|
|
aa6e536851 | ||
|
|
2a2c1af7ba | ||
|
|
48e381d702 | ||
|
|
9aafd76746 | ||
|
|
0069af78a3 | ||
|
|
c25fe7eb3d | ||
|
|
b9a9013685 | ||
|
|
54d075e4fd | ||
|
|
1c40044525 | ||
|
|
5784694dc9 | ||
|
|
7af4a6ff11 | ||
|
|
a601be0666 | ||
|
|
1be169a105 | ||
|
|
6b02af69f2 | ||
|
|
1fe4bd2de7 | ||
|
|
afd00a8ab6 | ||
|
|
63918b3c20 | ||
|
|
6293a4b936 | ||
|
|
44502092ad | ||
|
|
ce0e8284fe | ||
|
|
15f104ddd0 | ||
|
|
7f6ae8ffbf | ||
|
|
b2ecfb5a32 | ||
|
|
fa6ba8b668 | ||
|
|
826dffc794 | ||
|
|
688190ac4a | ||
|
|
4909a3b537 | ||
|
|
64e2150f44 | ||
|
|
3f7abc459c | ||
|
|
618cdafd10 | ||
|
|
e7b37d0378 | ||
|
|
3f3e2525d2 | ||
|
|
765e08f999 | ||
|
|
598bc29647 | ||
|
|
39ccddfb1c | ||
|
|
108214a217 | ||
|
|
271f40e355 | ||
|
|
e801547580 | ||
|
|
fc11941186 | ||
|
|
882fad3113 | ||
|
|
6168492711 | ||
|
|
2aade9aaa6 | ||
|
|
4789946c4e | ||
|
|
a69d8877c9 | ||
|
|
7cc0c3bc57 | ||
|
|
fb78495a1b | ||
|
|
22def5111f | ||
|
|
6e91d51def | ||
|
|
f3b8a4e931 | ||
|
|
69afa52beb | ||
|
|
f09a39d27c | ||
|
|
2e71963fbf | ||
|
|
f740a92fb7 | ||
|
|
21e7ddea16 | ||
|
|
923d90cf22 | ||
|
|
b386a1917d | ||
|
|
4a5427b2b2 | ||
|
|
1f2397b81a | ||
|
|
60d5aaaaa6 | ||
|
|
f506b840ed | ||
|
|
f357750d88 | ||
|
|
a2dbe68338 | ||
|
|
491d2cfab1 | ||
|
|
fa91abb739 | ||
|
|
6b46d04537 | ||
|
|
a6ff8e095a | ||
|
|
ffd2b4b76f | ||
|
|
c50ed843fb | ||
|
|
850ae5114f | ||
|
|
8b3267458b | ||
|
|
bccfaab350 | ||
|
|
753f12979e |
@@ -7,5 +7,14 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'prefer-regex-literals': 'warn',
|
'prefer-regex-literals': 'warn',
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'require-await': 'error',
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['migrations/**', 'gulp/**'], // Or *.test.js
|
||||||
|
rules: {
|
||||||
|
'require-await': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
9
.github/workflows/test.yml
vendored
@@ -1,6 +1,13 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'phillip/**'
|
||||||
|
- 'sabrecat/**'
|
||||||
|
- 'kalista/**'
|
||||||
|
- 'natalie/**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
|||||||
|
|
||||||
# mongodb replica set for local dev
|
# mongodb replica set for local dev
|
||||||
mongodb-*.tgz
|
mongodb-*.tgz
|
||||||
/mongodb-data
|
/mongodb-data*
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
||||||
|
|
||||||
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.
|
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
|
||||||
|
|||||||
@@ -8,18 +8,26 @@
|
|||||||
"AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
|
"AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
|
||||||
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
|
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
|
||||||
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
|
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
|
||||||
|
"APPLE_AUTH_CLIENT_ID": "",
|
||||||
|
"APPLE_AUTH_KEY_ID": "",
|
||||||
|
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||||
|
"APPLE_TEAM_ID": "",
|
||||||
"BASE_URL": "http://localhost:3000",
|
"BASE_URL": "http://localhost:3000",
|
||||||
|
"BLOCKED_IPS": "",
|
||||||
|
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||||
"CRON_SAFE_MODE": "false",
|
"CRON_SAFE_MODE": "false",
|
||||||
"CRON_SEMI_SAFE_MODE": "false",
|
"CRON_SEMI_SAFE_MODE": "false",
|
||||||
|
"DEBUG_ENABLED": "false",
|
||||||
"DISABLE_REQUEST_LOGGING": "true",
|
"DISABLE_REQUEST_LOGGING": "true",
|
||||||
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
|
||||||
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
|
||||||
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
|
||||||
"EMAIL_SERVER_AUTH_PASSWORD": "password",
|
"EMAIL_SERVER_AUTH_PASSWORD": "password",
|
||||||
"EMAIL_SERVER_AUTH_USER": "user",
|
"EMAIL_SERVER_AUTH_USER": "user",
|
||||||
"EMAIL_SERVER_URL": "http://example.com",
|
"EMAIL_SERVER_URL": "http://example.com",
|
||||||
|
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
||||||
|
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
||||||
|
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
||||||
"ENABLE_CONSOLE_LOGS_IN_PROD": "false",
|
"ENABLE_CONSOLE_LOGS_IN_PROD": "false",
|
||||||
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
|
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
|
||||||
|
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||||
"FACEBOOK_KEY": "123456789012345",
|
"FACEBOOK_KEY": "123456789012345",
|
||||||
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||||
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
|
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
|
||||||
@@ -29,15 +37,16 @@
|
|||||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||||
"IGNORE_REDIRECT": "true",
|
"IGNORE_REDIRECT": "true",
|
||||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||||
|
"LIVELINESS_PROBE_KEY": "",
|
||||||
|
"LOG_AMPLITUDE_EVENTS": "false",
|
||||||
|
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||||
"LOGGLY_CLIENT_TOKEN": "token",
|
"LOGGLY_CLIENT_TOKEN": "token",
|
||||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||||
"LOGGLY_TOKEN": "example-token",
|
"LOGGLY_TOKEN": "example-token",
|
||||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
|
||||||
"MAINTENANCE_MODE": "false",
|
"MAINTENANCE_MODE": "false",
|
||||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
|
||||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
|
||||||
"MONGODB_POOL_SIZE": "10",
|
"MONGODB_POOL_SIZE": "10",
|
||||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||||
|
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||||
"NODE_ENV": "development",
|
"NODE_ENV": "development",
|
||||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||||
@@ -55,43 +64,34 @@
|
|||||||
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
|
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
|
||||||
"PORT": 3000,
|
"PORT": 3000,
|
||||||
"PUSH_CONFIGS_APN_ENABLED": "false",
|
"PUSH_CONFIGS_APN_ENABLED": "false",
|
||||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
|
||||||
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
|
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
|
||||||
|
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||||
"PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
|
"PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
|
||||||
"PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
|
"PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
|
||||||
|
"RATE_LIMITER_ENABLED": "false",
|
||||||
|
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||||
|
"REDIS_PASSWORD": "12345678",
|
||||||
|
"REDIS_PORT": "1234",
|
||||||
"S3_ACCESS_KEY_ID": "accessKeyId",
|
"S3_ACCESS_KEY_ID": "accessKeyId",
|
||||||
"S3_BUCKET": "bucket",
|
"S3_BUCKET": "bucket",
|
||||||
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
||||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
|
||||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||||
|
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||||
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
||||||
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
|
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
|
||||||
|
"SKIP_SSL_CHECK_KEY": "key",
|
||||||
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||||
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||||
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
|
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
|
||||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||||
|
"SLOW_REQUEST_THRESHOLD": 1000,
|
||||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||||
"WEB_CONCURRENCY": 1,
|
|
||||||
"SKIP_SSL_CHECK_KEY": "key",
|
|
||||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
|
||||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
|
||||||
"APPLE_TEAM_ID": "",
|
|
||||||
"APPLE_AUTH_CLIENT_ID": "",
|
|
||||||
"APPLE_AUTH_KEY_ID": "",
|
|
||||||
"BLOCKED_IPS": "",
|
|
||||||
"LOG_AMPLITUDE_EVENTS": "false",
|
|
||||||
"RATE_LIMITER_ENABLED": "false",
|
|
||||||
"LIVELINESS_PROBE_KEY": "",
|
|
||||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
|
||||||
"REDIS_PORT": "1234",
|
|
||||||
"REDIS_PASSWORD": "12345678",
|
|
||||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
|
||||||
"TIME_TRAVEL_ENABLED": "false",
|
"TIME_TRAVEL_ENABLED": "false",
|
||||||
"DEBUG_ENABLED": "false",
|
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
"WEB_CONCURRENCY": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ services:
|
|||||||
dockerfile: ./Dockerfile-Dev
|
dockerfile: ./Dockerfile-Dev
|
||||||
command: ["npm", "start"]
|
command: ["npm", "start"]
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||||
networks:
|
networks:
|
||||||
@@ -33,7 +34,16 @@ services:
|
|||||||
- .:/usr/src/habitica
|
- .:/usr/src/habitica
|
||||||
- /usr/src/habitica/node_modules
|
- /usr/src/habitica/node_modules
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:3.6
|
image: mongo:5.0.23
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
|
||||||
|
healthcheck:
|
||||||
|
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||||
|
interval: 10s
|
||||||
|
timeout: 30s
|
||||||
|
start_period: 0s
|
||||||
|
start_interval: 1s
|
||||||
|
retries: 30
|
||||||
networks:
|
networks:
|
||||||
- habitica
|
- habitica
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ function filterFile (file) {
|
|||||||
if (file.relative.indexOf('icon_background') === 0) {
|
if (file.relative.indexOf('icon_background') === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (file.relative.indexOf('notif_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.relative.indexOf('quest_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.relative.indexOf('inventory_quest_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +80,7 @@ async function createSpritesStream (name, src) {
|
|||||||
const stream = mergeStream();
|
const stream = mergeStream();
|
||||||
// need to import this way bc of weird dependency things
|
// need to import this way bc of weird dependency things
|
||||||
// eslint-disable-next-line global-require
|
// eslint-disable-next-line global-require
|
||||||
const filter = require('gulp-filter');
|
const filter = require('gulp-filter').default;
|
||||||
|
|
||||||
const f = filter(filterFile);
|
const f = filter(filterFile);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import gulp from 'gulp';
|
|
||||||
import nodemon from 'gulp-nodemon';
|
|
||||||
|
|
||||||
import pkg from '../package.json';
|
|
||||||
|
|
||||||
gulp.task('nodemon', done => {
|
|
||||||
nodemon({
|
|
||||||
script: pkg.main,
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
@@ -49,12 +49,6 @@ function integrationTestCommand (testDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Test task definitions */
|
/* Test task definitions */
|
||||||
gulp.task('test:nodemon', gulp.series(done => {
|
|
||||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
|
||||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
|
||||||
done();
|
|
||||||
}, 'nodemon'));
|
|
||||||
|
|
||||||
gulp.task('test:prepare:mongo', cb => {
|
gulp.task('test:prepare:mongo', cb => {
|
||||||
const mongooseOptions = getDefaultConnectionOptions();
|
const mongooseOptions = getDefaultConnectionOptions();
|
||||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
|
|||||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
|
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-start'); // eslint-disable-line global-require
|
|
||||||
require('./gulp/gulp-tests'); // eslint-disable-line global-require
|
require('./gulp/gulp-tests'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
|
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
|
||||||
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
||||||
|
|||||||
115
migrations/archive/2024/20241119_gem_caps_hourglasses.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
const MIGRATION_NAME = '20241119_gem_caps_hourglasses';
|
||||||
|
import { model as User } from '../../../website/server/models/user';
|
||||||
|
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count += 1;
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
|
const { consecutive, customerId, dateTerminated, planId } = user.purchased.plan;
|
||||||
|
const isRecurring = customerId !== 'Gift' && !dateTerminated;
|
||||||
|
const updateOp = {
|
||||||
|
$set: {
|
||||||
|
migration: MIGRATION_NAME,
|
||||||
|
'purchased.plan.consecutive.gemCapExtra': Math.max(2 * Math.ceil((consecutive.gemCapExtra + 1) / 2, 26)),
|
||||||
|
},
|
||||||
|
$inc: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let hourglassBonus = 0;
|
||||||
|
|
||||||
|
if (isRecurring) {
|
||||||
|
await user.updateBalance(
|
||||||
|
5,
|
||||||
|
'admin_update_balance',
|
||||||
|
'',
|
||||||
|
'Subscription Reward Migration',
|
||||||
|
);
|
||||||
|
updateOp.$inc.balance = 5;
|
||||||
|
switch (planId) {
|
||||||
|
case 'basic':
|
||||||
|
case 'basic_earned':
|
||||||
|
case 'group_plan_auto':
|
||||||
|
hourglassBonus = 2;
|
||||||
|
break;
|
||||||
|
case 'basic_3mo':
|
||||||
|
case 'basic_6mo':
|
||||||
|
case 'google_6mo':
|
||||||
|
hourglassBonus = 4;
|
||||||
|
break;
|
||||||
|
case 'basic_12mo':
|
||||||
|
hourglassBonus = 12;
|
||||||
|
updateOp.$set['purchased.plan.hourglassPromoReceived'] = new Date();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hourglassBonus = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hourglassBonus) {
|
||||||
|
updateOp.$inc['purchased.plan.consecutive.trinkets'] = hourglassBonus;
|
||||||
|
await user.updateHourglasses(
|
||||||
|
hourglassBonus,
|
||||||
|
'admin_update_balance',
|
||||||
|
'',
|
||||||
|
'Subscription Reward Migration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_subscriber_reward',
|
||||||
|
title: 'Thanks for being a subscriber!',
|
||||||
|
text: 'Enjoy these extra Mystic Hourglasses and Gems to celebrate our new benefits.',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await User.updateOne(
|
||||||
|
{ _id: user._id },
|
||||||
|
updateOp,
|
||||||
|
).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function processUsers () {
|
||||||
|
let query = {
|
||||||
|
migration: { $ne: MIGRATION_NAME },
|
||||||
|
'purchased.plan.customerId': { $exists: true },
|
||||||
|
$or: [
|
||||||
|
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||||
|
{ 'purchased.plan.dateTerminated': null },
|
||||||
|
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
purchased: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({_id: 1})
|
||||||
|
.select(fields)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -37,7 +37,7 @@ let consoleStamp = require('console-stamp');
|
|||||||
consoleStamp(console);
|
consoleStamp(console);
|
||||||
|
|
||||||
// Initialize configuration
|
// Initialize configuration
|
||||||
require('../../website/server/libs/api-v3/setupNconf')();
|
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||||
|
|
||||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ let moment = require('moment');
|
|||||||
consoleStamp(console);
|
consoleStamp(console);
|
||||||
|
|
||||||
// Initialize configuration
|
// Initialize configuration
|
||||||
require('../../website/server/libs/api-v3/setupNconf')();
|
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||||
|
|
||||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
|
|||||||
function setUpServer () {
|
function setUpServer () {
|
||||||
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
||||||
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
||||||
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
|
const setupNconf = require('../website/server/libs/setupNconf').default; // eslint-disable-line global-require
|
||||||
|
|
||||||
setupNconf();
|
setupNconf();
|
||||||
|
|
||||||
// We require src/server and npt src/index because
|
// We require src/server and not src/index because
|
||||||
// 1. nconf is already setup
|
// 1. nconf is already setup
|
||||||
// 2. we don't need clustering
|
// 2. we don't need clustering
|
||||||
require('../website/server/server'); // eslint-disable-line global-require
|
require('../website/server/server'); // eslint-disable-line global-require
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function updateUser (user) {
|
|||||||
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
||||||
);
|
);
|
||||||
|
|
||||||
return User.update({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
return User.updateOne({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function processUsers () {
|
export default async function processUsers () {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const progressCount = 1000;
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Award users every extant pet and mount
|
* Award every extant piece of equippable gear
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function updateUser (user) {
|
async function updateUser (user) {
|
||||||
@@ -27,13 +27,13 @@ async function updateUser (user) {
|
|||||||
|
|
||||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function processUsers () {
|
export default async function processUsers () {
|
||||||
const query = {
|
const query = {
|
||||||
migration: { $ne: MIGRATION_NAME },
|
migration: { $ne: MIGRATION_NAME },
|
||||||
'auth.local.lowerCaseUsername': 'olson1',
|
'auth.local.username': 'ExampleHabitican',
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async function updateUser (user) {
|
|||||||
export default async function processUsers () {
|
export default async function processUsers () {
|
||||||
const query = {
|
const query = {
|
||||||
migration: { $ne: MIGRATION_NAME },
|
migration: { $ne: MIGRATION_NAME },
|
||||||
'auth.local.username': 'SabreTest',
|
'auth.local.username': 'ExampleHabitican',
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
|
|||||||
175
migrations/users/habitoween.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Award Habitoween ladder items to participants in this month's Habitoween festivities
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
|
||||||
|
const MIGRATION_NAME = '20241030_habitoween_ladder'; // Update when running in future years
|
||||||
|
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const set = { migration: MIGRATION_NAME };
|
||||||
|
const inc = {
|
||||||
|
'items.food.Candy_Skeleton': 1,
|
||||||
|
'items.food.Candy_Base': 1,
|
||||||
|
'items.food.Candy_CottonCandyBlue': 1,
|
||||||
|
'items.food.Candy_CottonCandyPink': 1,
|
||||||
|
'items.food.Candy_Shade': 1,
|
||||||
|
'items.food.Candy_White': 1,
|
||||||
|
'items.food.Candy_Golden': 1,
|
||||||
|
'items.food.Candy_Zombie': 1,
|
||||||
|
'items.food.Candy_Desert': 1,
|
||||||
|
'items.food.Candy_Red': 1,
|
||||||
|
};
|
||||||
|
const push = { notifications: { $each: [] } };
|
||||||
|
|
||||||
|
if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-RoyalPurple']) {
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_candy',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-RoyalPurple']) {
|
||||||
|
set['items.mounts.JackOLantern-RoyalPurple'] = true;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_purple_mount',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
|
||||||
|
set['items.pets.JackOLantern-RoyalPurple'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_purple_pet',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
|
||||||
|
set['items.mounts.JackOLantern-Glow'] = true;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_glow_mount',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
|
||||||
|
set['items.pets.JackOLantern-Glow'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_glow_pet',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||||
|
set['items.mounts.JackOLantern-Ghost'] = true;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_ghost_mount',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||||
|
set['items.pets.JackOLantern-Ghost'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_ghost_pet',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||||
|
set['items.mounts.JackOLantern-Base'] = true;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_base_mount',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set['items.pets.JackOLantern-Base'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_habitoween_base_pet',
|
||||||
|
title: 'Happy Habitoween!',
|
||||||
|
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
return User.updateOne({ _id: user._id }, { $inc: inc, $push: push, $set: set }).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function processUsers () {
|
||||||
|
const query = {
|
||||||
|
migration: { $ne: MIGRATION_NAME },
|
||||||
|
'auth.timestamps.loggedin': { $gt: new Date('2024-10-01') },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
items: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({ _id: 1 })
|
||||||
|
.select(fields)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
}
|
||||||
167
migrations/users/harvest_feast.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
|
||||||
|
const MIGRATION_NAME = '20241120_harvest_feast';
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const updateOp = {
|
||||||
|
$set: { migration: MIGRATION_NAME },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
|
||||||
|
updateOp.$inc = {
|
||||||
|
'items.food.Pie_Base': 1,
|
||||||
|
'items.food.Pie_CottonCandyBlue': 1,
|
||||||
|
'items.food.Pie_CottonCandyPink': 1,
|
||||||
|
'items.food.Pie_Desert': 1,
|
||||||
|
'items.food.Pie_Golden': 1,
|
||||||
|
'items.food.Pie_Red': 1,
|
||||||
|
'items.food.Pie_Shade': 1,
|
||||||
|
'items.food.Pie_Skeleton': 1,
|
||||||
|
'items.food.Pie_Zombie': 1,
|
||||||
|
'items.food.Pie_White': 1,
|
||||||
|
};
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_pie',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received an assortment of pie for your Pets!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_turkeyHelmGilded'] = true;
|
||||||
|
updateOp.$set['items.gear.owned.armor_special_turkeyArmorGilded'] = true;
|
||||||
|
updateOp.$set['items.gear.owned.back_special_turkeyTailGilded'] = true;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_gilded_set',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Gilded Turkey Armor, Helm, and Tail!',
|
||||||
|
destination: '/inventory/equipment',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_turkeyHelmBase'] = true;
|
||||||
|
updateOp.$set['items.gear.owned.armor_special_turkeyArmorBase'] = true;
|
||||||
|
updateOp.$set['items.gear.owned.back_special_turkeyTailBase'] = true;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_base_set',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Turkey Armor, Helm, and Tail!',
|
||||||
|
destination: '/inventory/equipment',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||||
|
updateOp.$set['items.mounts.Turkey-Gilded'] = true;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_gilded_mount',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Gilded Turkey Mount!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||||
|
updateOp.$set['items.pets.Turkey-Gilded'] = 5;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_gilded_pet',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Gilded Turkey Pet!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||||
|
updateOp.$set['items.mounts.Turkey-Base'] = true;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_base_mount',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Turkey Mount!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updateOp.$set['items.pets.Turkey-Base'] = 5;
|
||||||
|
updateOp.$push = {
|
||||||
|
notifications: {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'notif_harvestfeast_base_pet',
|
||||||
|
title: 'Happy Harvest Feast!',
|
||||||
|
text: 'Gobble gobble, you\'ve received the Turkey Pet!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
|
return User.updateOne({ _id: user._id }, updateOp).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function processUsers () {
|
||||||
|
const query = {
|
||||||
|
migration: { $ne: MIGRATION_NAME },
|
||||||
|
'auth.timestamps.loggedin': { $gt: new Date('2024-10-20') },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
items: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({ _id: 1 })
|
||||||
|
.select(fields)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
}
|
||||||
125
migrations/users/nye.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
|
||||||
|
const MIGRATION_NAME = '20231228_nye';
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const updateOp = {
|
||||||
|
$set: { migration: MIGRATION_NAME },
|
||||||
|
$push: { },
|
||||||
|
};
|
||||||
|
const data = {
|
||||||
|
title: 'Happy New Year!',
|
||||||
|
destination: '/inventory/equipment',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof user.items.gear.owned.head_special_nye2023 !== 'undefined') {
|
||||||
|
updateOp.$inc = {
|
||||||
|
'items.food.Candy_Skeleton': 1,
|
||||||
|
'items.food.Candy_Base': 1,
|
||||||
|
'items.food.Candy_CottonCandyBlue': 1,
|
||||||
|
'items.food.Candy_CottonCandyPink': 1,
|
||||||
|
'items.food.Candy_Shade': 1,
|
||||||
|
'items.food.Candy_White': 1,
|
||||||
|
'items.food.Candy_Golden': 1,
|
||||||
|
'items.food.Candy_Zombie': 1,
|
||||||
|
'items.food.Candy_Desert': 1,
|
||||||
|
'items.food.Candy_Red': 1,
|
||||||
|
};
|
||||||
|
data.icon = 'notif_candy_nye';
|
||||||
|
data.text = 'You’ve received an assortment of candy to celebrate with your Pets!';
|
||||||
|
data.destination = '/inventory/stable';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2023'] = true;
|
||||||
|
data.icon = 'notif_2023hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Ludicrous Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2022'] = true;
|
||||||
|
data.icon = 'notif_2022hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Fabulous Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2021'] = true;
|
||||||
|
data.icon = 'notif_2021hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Preposterous Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2020'] = true;
|
||||||
|
data.icon = 'notif_2020hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Extravagant Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2019'] = true;
|
||||||
|
data.icon = 'notif_2019hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Outrageous Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2018'] = true;
|
||||||
|
data.icon = 'notif_2018hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Outlandish Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2017'] = true;
|
||||||
|
data.icon = 'notif_2017hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Fanciful Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2016'] = true;
|
||||||
|
data.icon = 'notif_2016hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Whimsical Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2015'] = true;
|
||||||
|
data.icon = 'notif_2015hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Ridiculous Party Hat!';
|
||||||
|
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye2014'] = true;
|
||||||
|
data.icon = 'notif_2014hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Silly Party Hat!';
|
||||||
|
} else {
|
||||||
|
updateOp.$set['items.gear.owned.head_special_nye'] = true;
|
||||||
|
data.icon = 'notif_2013hat_nye';
|
||||||
|
data.text = 'Take on your resolutions with style in this Absurd Party Hat!';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOp.$push.notifications = {
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data,
|
||||||
|
seen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
|
return User.updateOne({ _id: user._id }, updateOp).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function processUsers () {
|
||||||
|
const query = {
|
||||||
|
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
|
||||||
|
migration: { $ne: MIGRATION_NAME },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
items: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({ _id: 1 })
|
||||||
|
.select(fields)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ import { v4 as uuid } from 'uuid';
|
|||||||
|
|
||||||
import { model as User } from '../../website/server/models/user';
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
|
||||||
const MIGRATION_NAME = '20181203_take_this';
|
const MIGRATION_NAME = 'YYYYMMDD_take_this';
|
||||||
|
const CHALLENGE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
|
||||||
|
|
||||||
const progressCount = 1000;
|
const progressCount = 1000;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -41,15 +42,15 @@ async function updateUser (user) {
|
|||||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
if (push) {
|
if (push) {
|
||||||
return User.update({ _id: user._id }, { $set: set, $push: push }).exec();
|
return User.updateOne({ _id: user._id }, { $set: set, $push: push }).exec();
|
||||||
}
|
}
|
||||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function processUsers () {
|
export default async function processUsers () {
|
||||||
const query = {
|
const query = {
|
||||||
migration: { $ne: MIGRATION_NAME },
|
migration: { $ne: MIGRATION_NAME },
|
||||||
challenges: '00708425-d477-41a5-bf27-6270466e7976',
|
challenges: CHALLENGE_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
|
|||||||
15260
package-lock.json
generated
40
package.json
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||||
"version": "5.27.3",
|
"version": "5.40.1",
|
||||||
"main": "./website/server/index.js",
|
"main": "./website/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.10",
|
||||||
"@babel/preset-env": "^7.22.10",
|
"@babel/preset-env": "^7.22.10",
|
||||||
"@babel/register": "^7.22.15",
|
"@babel/register": "^7.22.15",
|
||||||
|
"@google-analytics/data": "^4.12.1",
|
||||||
"@google-cloud/trace-agent": "^7.1.2",
|
"@google-cloud/trace-agent": "^7.1.2",
|
||||||
"@parse/node-apn": "^5.2.3",
|
"@parse/node-apn": "^5.2.3",
|
||||||
"@slack/webhook": "^6.1.0",
|
"@slack/webhook": "^6.1.0",
|
||||||
@@ -17,10 +18,10 @@
|
|||||||
"apple-auth": "^1.0.9",
|
"apple-auth": "^1.0.9",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.3",
|
||||||
"bootstrap": "^4.6.2",
|
"bootstrap": "^4.6.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.8.1",
|
||||||
"cookie-session": "^2.0.0",
|
"cookie-session": "^2.1.1",
|
||||||
"coupon-code": "^0.4.5",
|
"coupon-code": "^0.4.5",
|
||||||
"csv-stringify": "^5.6.5",
|
"csv-stringify": "^5.6.5",
|
||||||
"cwait": "^1.1.1",
|
"cwait": "^1.1.1",
|
||||||
@@ -28,17 +29,15 @@
|
|||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-config-habitrpg": "^6.2.3",
|
"eslint-config-habitrpg": "^6.2.3",
|
||||||
"eslint-plugin-mocha": "^5.0.0",
|
"eslint-plugin-mocha": "^5.0.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.21.1",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-validator": "^5.2.0",
|
"express-validator": "^5.2.0",
|
||||||
"firebase-admin": "^12.1.1",
|
"firebase-admin": "^12.1.1",
|
||||||
"glob": "^8.1.0",
|
"glob": "^8.1.0",
|
||||||
"got": "^11.8.6",
|
"got": "^11.8.6",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "5.0.1",
|
||||||
"gulp-babel": "^8.0.0",
|
"gulp-babel": "^8.0.0",
|
||||||
"gulp-filter": "^7.0.0",
|
"gulp-filter": "^9.0.0",
|
||||||
"gulp-imagemin": "^7.1.0",
|
|
||||||
"gulp-nodemon": "^2.5.0",
|
|
||||||
"gulp.spritesmith": "^6.13.0",
|
"gulp.spritesmith": "^6.13.0",
|
||||||
"habitica-markdown": "^3.0.0",
|
"habitica-markdown": "^3.0.0",
|
||||||
"helmet": "^4.6.0",
|
"helmet": "^4.6.0",
|
||||||
@@ -50,13 +49,12 @@
|
|||||||
"merge-stream": "^2.0.0",
|
"merge-stream": "^2.0.0",
|
||||||
"method-override": "^3.0.0",
|
"method-override": "^3.0.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-recur": "^1.0.7",
|
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||||
"mongoose": "^7.6.3",
|
"mongoose": "^8.9.5",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.1",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"node-gcm": "^1.0.5",
|
"node-gcm": "^1.0.5",
|
||||||
"nodemon": "^2.0.20",
|
"on-headers": "^1.1.0",
|
||||||
"on-headers": "^1.0.2",
|
|
||||||
"passport": "^0.5.3",
|
"passport": "^0.5.3",
|
||||||
"passport-facebook": "^3.0.0",
|
"passport-facebook": "^3.0.0",
|
||||||
"passport-google-oauth2": "^0.2.0",
|
"passport-google-oauth2": "^0.2.0",
|
||||||
@@ -72,7 +70,6 @@
|
|||||||
"sinon": "^15.2.0",
|
"sinon": "^15.2.0",
|
||||||
"stripe": "^12.18.0",
|
"stripe": "^12.18.0",
|
||||||
"superagent": "^8.1.2",
|
"superagent": "^8.1.2",
|
||||||
"universal-analytics": "^0.5.3",
|
|
||||||
"useragent": "^2.1.9",
|
"useragent": "^2.1.9",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
@@ -100,26 +97,27 @@
|
|||||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||||
"test:nodemon": "gulp test:nodemon",
|
|
||||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||||
"sprites": "gulp sprites:compile",
|
"sprites": "gulp sprites:compile",
|
||||||
"client:dev": "cd website/client && npm run serve",
|
"client:dev": "cd website/client && npm run serve",
|
||||||
"client:build": "cd website/client && npm run build",
|
"client:build": "cd website/client && npm run build",
|
||||||
"client:unit": "cd website/client && npm run test:unit",
|
"client:unit": "cd website/client && npm run test:unit",
|
||||||
"start": "gulp nodemon",
|
"start": "node --watch ./website/server/index.js",
|
||||||
"debug": "gulp nodemon --inspect",
|
"start:simple": "node ./website/server/index.js",
|
||||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
"debug": "node --watch --inspect ./website/server/index.js",
|
||||||
|
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||||
|
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||||
"apidoc": "gulp apidoc",
|
"apidoc": "gulp apidoc",
|
||||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.8.2",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-moment": "^0.1.0",
|
"chai-moment": "^0.1.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.5",
|
||||||
"mocha": "^5.1.1",
|
"mocha": "^5.1.1",
|
||||||
"monk": "^7.3.4",
|
"monk": "^7.3.4",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ async function deleteHabiticaData (user, email) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processEmailAddress (email) {
|
async function processEmailAddress (email) {
|
||||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
|
||||||
const localUsers = await User.find(
|
const localUsers = await User.find(
|
||||||
{ 'auth.local.email': emailRegex },
|
{ 'auth.local.email': email },
|
||||||
{ _id: 1, apiToken: 1, auth: 1 },
|
{ _id: 1, apiToken: 1, auth: 1 },
|
||||||
).exec();
|
).exec();
|
||||||
|
|
||||||
const socialUsers = await User.find(
|
const socialUsers = await User.find(
|
||||||
{
|
{
|
||||||
'auth.local.email': { $not: emailRegex },
|
'auth.local.email': { $ne: email },
|
||||||
$or: [
|
$or: [
|
||||||
{ 'auth.facebook.emails.value': email },
|
{ 'auth.facebook.emails.value': email },
|
||||||
{ 'auth.google.emails.value': email },
|
{ 'auth.google.emails.value': email },
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ const TASK_VALUE_CHANGE_FACTOR = 0.9747;
|
|||||||
const MIN_TASK_VALUE = -47.27;
|
const MIN_TASK_VALUE = -47.27;
|
||||||
|
|
||||||
async function updateTeamTasks (team) {
|
async function updateTeamTasks (team) {
|
||||||
|
if (team.purchased.plan.dateTerminated) {
|
||||||
|
const dateTerminated = new Date(team.purchased.plan.dateTerminated);
|
||||||
|
if (dateTerminated < new Date()) {
|
||||||
|
team.purchased.plan.customerId = undefined;
|
||||||
|
team.markModified('purchased.plan');
|
||||||
|
return team.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toSave = [];
|
const toSave = [];
|
||||||
|
|
||||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||||
|
|
||||||
if (!teamLeader) { // why would this happen?
|
if (!teamLeader) { // why would this happen?
|
||||||
@@ -93,12 +103,7 @@ async function updateTeamTasks (team) {
|
|||||||
export default async function processTeamsCron () {
|
export default async function processTeamsCron () {
|
||||||
const activeTeams = await Group.find({
|
const activeTeams = await Group.find({
|
||||||
'purchased.plan.customerId': { $exists: true },
|
'purchased.plan.customerId': { $exists: true },
|
||||||
$or: [
|
}, { cron: 1, leader: 1, purchased: 1 }).exec();
|
||||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
|
||||||
{ 'purchased.plan.dateTerminated': null },
|
|
||||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
|
||||||
],
|
|
||||||
}).exec();
|
|
||||||
|
|
||||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||||
return Promise.all(cronPromises);
|
return Promise.all(cronPromises);
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import Amplitude from 'amplitude';
|
import Amplitude from 'amplitude';
|
||||||
import { Visitor } from 'universal-analytics';
|
|
||||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||||
|
|
||||||
describe('analyticsService', () => {
|
describe('analyticsService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||||
|
|
||||||
sandbox.stub(Visitor.prototype, 'event');
|
|
||||||
sandbox.stub(Visitor.prototype, 'transaction');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -37,8 +33,6 @@ describe('analyticsService', () => {
|
|||||||
data;
|
data;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Visitor.prototype.event.yields();
|
|
||||||
|
|
||||||
eventType = 'Cron';
|
eventType = 'Cron';
|
||||||
data = {
|
data = {
|
||||||
category: 'behavior',
|
category: 'behavior',
|
||||||
@@ -49,6 +43,11 @@ describe('analyticsService', () => {
|
|||||||
'x-client': 'habitica-web',
|
'x-client': 'habitica-web',
|
||||||
'user-agent': '',
|
'user-agent': '',
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
preferences: {
|
||||||
|
analyticsConsent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,6 +294,9 @@ describe('analyticsService', () => {
|
|||||||
rewards: [{ _id: 'reward' }],
|
rewards: [{ _id: 'reward' }],
|
||||||
balance: 12,
|
balance: 12,
|
||||||
loginIncentives: 1,
|
loginIncentives: 1,
|
||||||
|
preferences: {
|
||||||
|
analyticsConsent: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
data.user = user;
|
data.user = user;
|
||||||
@@ -326,37 +328,12 @@ describe('analyticsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('GA', () => {
|
|
||||||
it('calls out to GA', () => analyticsService.track(eventType, data)
|
|
||||||
.then(() => {
|
|
||||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
|
||||||
.then(() => {
|
|
||||||
expect(Visitor.prototype.event).to.be.calledWith({
|
|
||||||
ea: 'Cron',
|
|
||||||
ec: 'behavior',
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#trackPurchase', () => {
|
describe('#trackPurchase', () => {
|
||||||
let data; let
|
let data;
|
||||||
itemSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Visitor.prototype.event.yields();
|
|
||||||
|
|
||||||
itemSpy = sandbox.stub().returnsThis();
|
|
||||||
|
|
||||||
Visitor.prototype.transaction.returns({
|
|
||||||
item: itemSpy,
|
|
||||||
send: sandbox.stub().yields(),
|
|
||||||
});
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
uuid: 'user-id',
|
uuid: 'user-id',
|
||||||
sku: 'paypal-checkout',
|
sku: 'paypal-checkout',
|
||||||
@@ -370,6 +347,11 @@ describe('analyticsService', () => {
|
|||||||
'x-client': 'habitica-web',
|
'x-client': 'habitica-web',
|
||||||
'user-agent': '',
|
'user-agent': '',
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
preferences: {
|
||||||
|
analyticsConsent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -533,6 +515,9 @@ describe('analyticsService', () => {
|
|||||||
dailys: [{ _id: 'daily' }],
|
dailys: [{ _id: 'daily' }],
|
||||||
todos: [{ _id: 'todo' }],
|
todos: [{ _id: 'todo' }],
|
||||||
rewards: [{ _id: 'reward' }],
|
rewards: [{ _id: 'reward' }],
|
||||||
|
preferences: {
|
||||||
|
analyticsConsent: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
data.user = user;
|
data.user = user;
|
||||||
@@ -561,26 +546,6 @@ describe('analyticsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('GA', () => {
|
|
||||||
it('calls out to GA', () => analyticsService.trackPurchase(data)
|
|
||||||
.then(() => {
|
|
||||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
|
||||||
expect(Visitor.prototype.transaction).to.be.calledOnce;
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
|
||||||
.then(() => {
|
|
||||||
expect(Visitor.prototype.event).to.be.calledWith({
|
|
||||||
ea: 'checkout',
|
|
||||||
ec: 'commerce',
|
|
||||||
el: 'PayPal',
|
|
||||||
ev: 8,
|
|
||||||
});
|
|
||||||
expect(Visitor.prototype.transaction).to.be.calledWith('user-id', 8);
|
|
||||||
expect(itemSpy).to.be.calledWith(8, 1, 'paypal-checkout', 'Gems', 'checkout');
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mockAnalyticsService', () => {
|
describe('mockAnalyticsService', () => {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe('bug-report', () => {
|
|||||||
emailData: {
|
emailData: {
|
||||||
BROWSER_UA: userAgent,
|
BROWSER_UA: userAgent,
|
||||||
REPORT_MSG: userMessage,
|
REPORT_MSG: userMessage,
|
||||||
|
USER_ANALYTICS: undefined,
|
||||||
USER_CLASS: 'warrior',
|
USER_CLASS: 'warrior',
|
||||||
USER_CONSECUTIVE_MONTHS: 0,
|
USER_CONSECUTIVE_MONTHS: 0,
|
||||||
USER_COSTUME: 'false',
|
USER_COSTUME: 'false',
|
||||||
@@ -44,7 +45,6 @@ describe('bug-report', () => {
|
|||||||
USER_HOURGLASSES: 0,
|
USER_HOURGLASSES: 0,
|
||||||
USER_ID: userId,
|
USER_ID: userId,
|
||||||
USER_LEVEL: 1,
|
USER_LEVEL: 1,
|
||||||
USER_OFFSET_MONTHS: 0,
|
|
||||||
USER_PAYMENT_PLATFORM: undefined,
|
USER_PAYMENT_PLATFORM: undefined,
|
||||||
USER_SUBSCRIPTION: undefined,
|
USER_SUBSCRIPTION: undefined,
|
||||||
USER_TIMEZONE_OFFSET: 0,
|
USER_TIMEZONE_OFFSET: 0,
|
||||||
|
|||||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
|||||||
expect(got.post).not.to.be.called;
|
expect(got.post).not.to.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when mail target is only a string', () => {
|
it('throws error when mail target is only a string', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = 'my email';
|
const mailingInfo = 'my email';
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when mail target has no _id or email', () => {
|
it('throws error when mail target has no _id or email', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when variables not an array', () => {
|
it('throws error when variables not an array', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
@@ -195,9 +195,10 @@ describe('emails', () => {
|
|||||||
};
|
};
|
||||||
const variables = {};
|
const variables = {};
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
|
||||||
});
|
});
|
||||||
it('throws error when variables array not contain name/content', () => {
|
|
||||||
|
it('throws error when variables array not contain name/content', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
@@ -209,8 +210,9 @@ describe('emails', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws no error when variables array contain name but no content', () => {
|
it('throws no error when variables array contain name but no content', () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import nconf from 'nconf';
|
|
||||||
import requireAgain from 'require-again';
|
import requireAgain from 'require-again';
|
||||||
|
|
||||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
|||||||
expect(string).to.equal('mongodb://hostname:3030');
|
expect(string).to.equal('mongodb://hostname:3030');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getDefaultConnectionOptions', () => {
|
|
||||||
it('returns development config when IS_PROD is false', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
|
||||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
|
||||||
|
|
||||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
|
||||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns production config when IS_PROD is true', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
|
||||||
|
|
||||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
|
||||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -715,7 +715,7 @@ describe('Purchasing a group plan for group', () => {
|
|||||||
const mysteryItem = { title: 'item' };
|
const mysteryItem = { title: 'item' };
|
||||||
const mysteryItems = [mysteryItem];
|
const mysteryItems = [mysteryItem];
|
||||||
const consecutive = {
|
const consecutive = {
|
||||||
trinkets: 3,
|
trinkets: 4,
|
||||||
gemCapExtra: 20,
|
gemCapExtra: 20,
|
||||||
offset: 1,
|
offset: 1,
|
||||||
count: 13,
|
count: 13,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../../../../helpers/api-unit.helper';
|
} from '../../../../helpers/api-unit.helper';
|
||||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||||
|
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||||
|
|
||||||
describe('payments/index', () => {
|
describe('payments/index', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -65,7 +66,6 @@ describe('payments/index', () => {
|
|||||||
mysteryItems: [],
|
mysteryItems: [],
|
||||||
consecutive: {
|
consecutive: {
|
||||||
trinkets: 0,
|
trinkets: 0,
|
||||||
offset: 0,
|
|
||||||
gemCapExtra: 0,
|
gemCapExtra: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -108,14 +108,8 @@ describe('payments/index', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('add a transaction entry to the recipient', async () => {
|
it('add a transaction entry to the recipient', async () => {
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
|
||||||
|
|
||||||
const transactions = await TransactionModel
|
const transactions = await TransactionModel
|
||||||
.find({ userId: recipient._id })
|
.find({ userId: recipient._id })
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
@@ -177,6 +171,45 @@ describe('payments/index', () => {
|
|||||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not reset gemCapExtra if they already had one', async () => {
|
||||||
|
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets gemCapExtra to 0 if they receive a 3 month sub', async () => {
|
||||||
|
data.gift.subscription.key = 'basic_3mo';
|
||||||
|
data.gift.subscription.months = 3;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets gemCapExtra to max if they receive a 12 month sub', async () => {
|
||||||
|
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||||
|
|
||||||
|
data.gift.subscription.key = 'basic_12mo';
|
||||||
|
data.gift.subscription.months = 12;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives user 1 hourglass if they have no active subscription', async () => {
|
||||||
|
await api.createSubscription(data);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not give any hourglasses if they have an active subscription', async () => {
|
||||||
|
recipient.purchased.plan = plan;
|
||||||
|
await api.createSubscription(data);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(plan.consecutive.trinkets);
|
||||||
|
});
|
||||||
|
|
||||||
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
||||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||||
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||||
@@ -235,116 +268,6 @@ describe('payments/index', () => {
|
|||||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = 1;
|
|
||||||
recipient.purchased.plan.customerId = undefined;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.gift.subscription.key = 'basic_earned';
|
|
||||||
data.gift.subscription.months = 1;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = -1;
|
|
||||||
recipient.purchased.plan.customerId = undefined;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.gift.subscription.key = 'basic_earned';
|
|
||||||
data.gift.subscription.months = 1;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = 2;
|
|
||||||
recipient.purchased.plan.customerId = undefined;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.gift.subscription.key = 'basic_earned';
|
|
||||||
data.gift.subscription.months = 1;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = 1;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.gift.subscription.key = 'basic_earned';
|
|
||||||
data.gift.subscription.months = 1;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = 2;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.gift.subscription.key = 'basic_earned';
|
|
||||||
data.gift.subscription.months = 1;
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
|
|
||||||
recipient.purchased.plan.perkMonthCount = 0;
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('awards perks if plan.perkMonthCount goes over 3', async () => {
|
|
||||||
recipient.purchased.plan = plan;
|
|
||||||
recipient.purchased.plan.perkMonthCount = 2;
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
|
||||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
expect(recipient.purchased.plan.customerId).to.not.exist;
|
||||||
|
|
||||||
@@ -421,8 +344,8 @@ describe('payments/index', () => {
|
|||||||
context('Active Promotion', () => {
|
context('Active Promotion', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(worldState, 'getCurrentEventList').returns([{
|
sinon.stub(worldState, 'getCurrentEventList').returns([{
|
||||||
...common.content.events.winter2021Promo,
|
...REPEATING_EVENTS.giftOneGetOne,
|
||||||
event: 'winter2021',
|
event: 'g1g1',
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -438,22 +361,30 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||||
expect(user.purchased.plan.dateCreated).to.exist;
|
expect(user.purchased.plan.dateCreated).to.exist;
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
||||||
user.purchased.plan = plan;
|
user.purchased.plan = plan;
|
||||||
|
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||||
@@ -466,10 +397,12 @@ describe('payments/index', () => {
|
|||||||
recipient.purchased.plan = plan;
|
recipient.purchased.plan = plan;
|
||||||
|
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
|
||||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||||
@@ -484,11 +417,15 @@ describe('payments/index', () => {
|
|||||||
|
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends a private message about the promotion', async () => {
|
it('sends a private message about the promotion', async () => {
|
||||||
@@ -511,7 +448,6 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
expect(user.purchased.plan.gemsBought).to.eql(0);
|
||||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||||
@@ -549,33 +485,6 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps plan.perkMonthCount when changing subscription type', async () => {
|
|
||||||
await api.createSubscription(data);
|
|
||||||
user.purchased.plan.perkMonthCount = 2;
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.perkMonthCount).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
|
|
||||||
user.purchased.plan.perkMonthCount = 2;
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
|
|
||||||
user.purchased.plan.perkMonthCount = 2;
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates plan.consecutive.offset when changing subscription type', async () => {
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.consecutive.offset).to.eql(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('awards the Royal Purple Jackalope pet', async () => {
|
it('awards the Royal Purple Jackalope pet', async () => {
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
@@ -694,6 +603,7 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -741,55 +651,20 @@ describe('payments/index', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context('Block subscription perks', () => {
|
context('Block subscription perks', () => {
|
||||||
it('adds block months to plan.consecutive.offset', async () => {
|
it('adds 26 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resets plans.consecutive.offset if 1 month subscription', async () => {
|
|
||||||
user.purchased.plan.consecutive.offset = 1;
|
|
||||||
await user.save();
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not raise plan.consecutive.gemCapExtra higher than 25', async () => {
|
it('does not raise plan.consecutive.gemCapExtra higher than 26', async () => {
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
|
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
|
||||||
@@ -798,20 +673,29 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds 2 plan.consecutive.trinkets for 6 month block', async () => {
|
it('adds 1 plan.consecutive.trinkets for 6 month block', async () => {
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds 4 plan.consecutive.trinkets for 12 month block', async () => {
|
it('adds 1 plan.consecutive.trinkets for 12 month block if they had promo', async () => {
|
||||||
|
user.purchased.plan.hourglassPromoReceived = new Date();
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds 12 plan.consecutive.trinkets for 12 month block', async () => {
|
||||||
|
data.sub.key = 'basic_12mo';
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Upgrades subscription', () => {
|
context('Upgrades subscription', () => {
|
||||||
@@ -819,70 +703,38 @@ describe('payments/index', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
data.updatedFrom = { logic: 'payDifference' };
|
data.updatedFrom = { logic: 'payDifference' };
|
||||||
});
|
});
|
||||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -894,7 +746,7 @@ describe('payments/index', () => {
|
|||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -902,70 +754,39 @@ describe('payments/index', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
data.updatedFrom = { logic: 'payFull' };
|
data.updatedFrom = { logic: 'payFull' };
|
||||||
});
|
});
|
||||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -977,7 +798,7 @@ describe('payments/index', () => {
|
|||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -988,30 +809,13 @@ describe('payments/index', () => {
|
|||||||
data.updatedFrom = { logic: 'refundAndRepay' };
|
data.updatedFrom = { logic: 'refundAndRepay' };
|
||||||
});
|
});
|
||||||
context('Upgrades within first half of subscription', () => {
|
context('Upgrades within first half of subscription', () => {
|
||||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
clock.restore();
|
|
||||||
clock = sinon.useFakeTimers(new Date('2022-01-10'));
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
@@ -1019,28 +823,10 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-02-05'));
|
clock = sinon.useFakeTimers(new Date('2022-02-05'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
clock.restore();
|
|
||||||
clock = sinon.useFakeTimers(new Date('2022-01-08'));
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -1054,17 +840,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-01-31'));
|
clock = sinon.useFakeTimers(new Date('2022-01-31'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
@@ -1072,35 +858,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
it('2 plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
data.updatedFrom.key = 'basic_earned';
|
|
||||||
clock.restore();
|
|
||||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
@@ -1108,10 +876,10 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-08-28'));
|
clock = sinon.useFakeTimers(new Date('2022-08-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -1125,11 +893,11 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-07-31'));
|
clock = sinon.useFakeTimers(new Date('2022-07-31'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
context('Upgrades within second half of subscription', () => {
|
context('Upgrades within second half of subscription', () => {
|
||||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
it('Adds 0 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
data.sub.key = 'basic_earned';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
@@ -1144,16 +912,16 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-01-20'));
|
clock = sinon.useFakeTimers(new Date('2022-01-20'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_3mo';
|
data.updatedFrom.key = 'basic_3mo';
|
||||||
@@ -1161,17 +929,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-02-24'));
|
clock = sinon.useFakeTimers(new Date('2022-02-24'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
data.sub.key = 'basic_earned';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
data.updatedFrom.key = 'basic_earned';
|
data.updatedFrom.key = 'basic_earned';
|
||||||
@@ -1179,17 +947,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
@@ -1197,10 +965,10 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -1214,17 +982,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-03-03'));
|
clock = sinon.useFakeTimers(new Date('2022-03-03'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||||
data.sub.key = 'basic_earned';
|
data.sub.key = 'basic_earned';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
data.updatedFrom.key = 'basic_earned';
|
data.updatedFrom.key = 'basic_earned';
|
||||||
@@ -1232,17 +1000,17 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
data.updatedFrom.key = 'basic_6mo';
|
data.updatedFrom.key = 'basic_6mo';
|
||||||
@@ -1250,10 +1018,10 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2023-05-28'));
|
clock = sinon.useFakeTimers(new Date('2023-05-28'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -1267,7 +1035,7 @@ describe('payments/index', () => {
|
|||||||
clock = sinon.useFakeTimers(new Date('2023-09-03'));
|
clock = sinon.useFakeTimers(new Date('2023-09-03'));
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -1277,22 +1045,6 @@ describe('payments/index', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context('Downgrades subscription', () => {
|
context('Downgrades subscription', () => {
|
||||||
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.updatedFrom = { key: 'basic_6mo' };
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
|
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
expect(user.purchased.plan.planId).to.not.exist;
|
||||||
|
|
||||||
@@ -1300,28 +1052,12 @@ describe('payments/index', () => {
|
|||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
|
|
||||||
data.sub.key = 'basic_3mo';
|
data.sub.key = 'basic_3mo';
|
||||||
data.updatedFrom = { key: 'basic_12mo' };
|
data.updatedFrom = { key: 'basic_12mo' };
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||||
});
|
|
||||||
|
|
||||||
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
|
|
||||||
data.sub.key = 'basic_6mo';
|
|
||||||
expect(user.purchased.plan.planId).to.not.exist;
|
|
||||||
|
|
||||||
await api.createSubscription(data);
|
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
|
|
||||||
data.sub.key = 'basic_earned';
|
|
||||||
data.updatedFrom = { key: 'basic_6mo' };
|
|
||||||
await api.createSubscription(data);
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
|
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
|
||||||
@@ -1331,12 +1067,12 @@ describe('payments/index', () => {
|
|||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
|
|
||||||
data.sub.key = 'basic_3mo';
|
data.sub.key = 'basic_3mo';
|
||||||
data.updatedFrom = { key: 'basic_12mo' };
|
data.updatedFrom = { key: 'basic_12mo' };
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1453,6 +1189,32 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not reset gemCapExtra', async () => {
|
||||||
|
user.purchased.plan.consecutive.gemCapExtra = 12;
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes gemCapExtra', async () => {
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes hourglasses', async () => {
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reset owned hourglasses', async () => {
|
||||||
|
user.purchased.plan.consecutive.trinkets = 12;
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(12);
|
||||||
|
});
|
||||||
|
|
||||||
it('sends an email', async () => {
|
it('sends an email', async () => {
|
||||||
await api.cancelSubscription(data);
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
gift: undefined,
|
gift: undefined,
|
||||||
sub: undefined,
|
sub: undefined,
|
||||||
gemsBlock: gemsBlockKey,
|
gemsBlock: gemsBlockKey,
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(gems.validateGiftMessage).to.not.be.called;
|
expect(gems.validateGiftMessage).to.not.be.called;
|
||||||
@@ -101,6 +102,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
gift: JSON.stringify(gift),
|
gift: JSON.stringify(gift),
|
||||||
sub: undefined,
|
sub: undefined,
|
||||||
gemsBlock: undefined,
|
gemsBlock: undefined,
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||||
@@ -155,6 +157,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
gift: JSON.stringify(gift),
|
gift: JSON.stringify(gift),
|
||||||
sub: undefined,
|
sub: undefined,
|
||||||
gemsBlock: undefined,
|
gemsBlock: undefined,
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||||
@@ -192,6 +195,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
userId: user._id,
|
userId: user._id,
|
||||||
gift: undefined,
|
gift: undefined,
|
||||||
sub: JSON.stringify(sub),
|
sub: JSON.stringify(sub),
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||||
@@ -258,6 +262,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
userId: user._id,
|
userId: user._id,
|
||||||
gift: undefined,
|
gift: undefined,
|
||||||
sub: JSON.stringify(sub),
|
sub: JSON.stringify(sub),
|
||||||
|
server_url: BASE_URL,
|
||||||
groupId,
|
groupId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,8 +333,9 @@ describe('Stripe - Checkout', () => {
|
|||||||
user.purchased.plan.customerId = customerId;
|
user.purchased.plan.customerId = customerId;
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
userId: user._id,
|
|
||||||
type: 'edit-card-user',
|
type: 'edit-card-user',
|
||||||
|
userId: user._id,
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await createEditCardCheckoutSession({ user }, stripe);
|
const res = await createEditCardCheckoutSession({ user }, stripe);
|
||||||
@@ -418,6 +424,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
const metadata = {
|
const metadata = {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
type: 'edit-card-group',
|
type: 'edit-card-group',
|
||||||
|
server_url: BASE_URL,
|
||||||
groupId,
|
groupId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -455,6 +462,7 @@ describe('Stripe - Checkout', () => {
|
|||||||
userId: anotherUser._id,
|
userId: anotherUser._id,
|
||||||
type: 'edit-card-group',
|
type: 'edit-card-group',
|
||||||
groupId,
|
groupId,
|
||||||
|
server_url: BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);
|
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ describe('Stripe - One Time Payments', () => {
|
|||||||
customerId,
|
customerId,
|
||||||
paymentMethod: 'Gift',
|
paymentMethod: 'Gift',
|
||||||
gift,
|
gift,
|
||||||
|
autoRenews: false,
|
||||||
gemsBlock: undefined,
|
gemsBlock: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ describe('Stripe - Subscriptions', () => {
|
|||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
sub: sinon.match({ ...sub }),
|
sub: sinon.match({ ...sub }),
|
||||||
groupId: null,
|
groupId: null,
|
||||||
|
autoRenews: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,6 +198,7 @@ describe('Stripe - Subscriptions', () => {
|
|||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
sub: sinon.match({ ...sub }),
|
sub: sinon.match({ ...sub }),
|
||||||
groupId,
|
groupId,
|
||||||
|
autoRenews: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,6 +233,7 @@ describe('Stripe - Subscriptions', () => {
|
|||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
sub: sinon.match({ ...sub }),
|
sub: sinon.match({ ...sub }),
|
||||||
groupId,
|
groupId,
|
||||||
|
autoRenews: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import * as subscriptions from '../../../../../../website/server/libs/payments/s
|
|||||||
const { i18n } = common;
|
const { i18n } = common;
|
||||||
|
|
||||||
describe('Stripe - Webhooks', () => {
|
describe('Stripe - Webhooks', () => {
|
||||||
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
const stripe = stripeModule('test');
|
const stripe = stripeModule('test');
|
||||||
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@@ -284,7 +285,9 @@ describe('Stripe - Webhooks', () => {
|
|||||||
const session = {};
|
const session = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
session.metadata = {};
|
session.metadata = {
|
||||||
|
server_url: BASE_URL,
|
||||||
|
};
|
||||||
event = { type: eventType, data: { object: session } };
|
event = { type: eventType, data: { object: session } };
|
||||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||||
constructEventStub.returns(event);
|
constructEventStub.returns(event);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import nconf from 'nconf';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
import {
|
import {
|
||||||
generateRes,
|
generateRes,
|
||||||
generateReq,
|
generateReq,
|
||||||
} from '../../../helpers/api-unit.helper';
|
} from '../../../helpers/api-unit.helper';
|
||||||
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
|
|
||||||
|
const authPath = '../../../../website/server/middlewares/auth';
|
||||||
|
|
||||||
describe('auth middleware', () => {
|
describe('auth middleware', () => {
|
||||||
let res; let req; let
|
let res; let req; let
|
||||||
@@ -16,6 +19,7 @@ describe('auth middleware', () => {
|
|||||||
|
|
||||||
describe('auth with headers', () => {
|
describe('auth with headers', () => {
|
||||||
it('allows to specify a list of user field that we do not want to load', done => {
|
it('allows to specify a list of user field that we do not want to load', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
const authWithHeaders = authWithHeadersFactory({
|
const authWithHeaders = authWithHeadersFactory({
|
||||||
userFieldsToExclude: ['items'],
|
userFieldsToExclude: ['items'],
|
||||||
});
|
});
|
||||||
@@ -35,6 +39,7 @@ describe('auth middleware', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('makes sure some fields are always included', done => {
|
it('makes sure some fields are always included', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
const authWithHeaders = authWithHeadersFactory({
|
const authWithHeaders = authWithHeadersFactory({
|
||||||
userFieldsToExclude: [
|
userFieldsToExclude: [
|
||||||
'items', 'auth.timestamps',
|
'items', 'auth.timestamps',
|
||||||
@@ -60,5 +65,57 @@ describe('auth middleware', () => {
|
|||||||
return done();
|
return done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors with InvalidCredentialsError and code when token is wrong', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
|
const authWithHeaders = authWithHeadersFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = 'totally-wrong-token';
|
||||||
|
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(err.name).to.equal('InvalidCredentialsError');
|
||||||
|
expect(err.code).to.equal('invalid_credentials');
|
||||||
|
expect(err.message).to.equal(res.t('invalidCredentials'));
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when ENFORCE_CLIENT_HEADER is true', () => {
|
||||||
|
let authFactory;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('ENFORCE_CLIENT_HEADER').returns('true');
|
||||||
|
authFactory = requireAgain(authPath).authWithHeaders;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors with missingClientHeader when x-client header is not present', done => {
|
||||||
|
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = user;
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(err.name).to.equal('BadRequest');
|
||||||
|
expect(err.message).to.equal(res.t('missingClientHeader'));
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows request to pass when x-client header is present', done => {
|
||||||
|
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = user.apiToken;
|
||||||
|
req.headers['x-client'] = 'habitica-web';
|
||||||
|
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.locals.user).to.exist;
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
197
test/api/unit/middlewares/blocker.test.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import nconf from 'nconf';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateNext,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
import { Forbidden } from '../../../../website/server/libs/errors';
|
||||||
|
import { apiError } from '../../../../website/server/libs/apiError';
|
||||||
|
import { model as Blocker } from '../../../../website/server/models/blocker';
|
||||||
|
|
||||||
|
function checkIPBlockedErrorThrown (next) {
|
||||||
|
expect(next).to.have.been.calledOnce;
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0].message).to.equal(apiError('ipAddressBlocked'));
|
||||||
|
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkClientBlockedErrorThrown (next) {
|
||||||
|
expect(next).to.have.been.calledOnce;
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0].message).to.equal(apiError('clientBlocked'));
|
||||||
|
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkErrorNotThrown (next) {
|
||||||
|
expect(next).to.have.been.calledOnce;
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Blocker middleware', () => {
|
||||||
|
const pathToBlocker = '../../../../website/server/middlewares/blocker';
|
||||||
|
|
||||||
|
let res; let req; let next;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
res = generateRes();
|
||||||
|
req = generateReq();
|
||||||
|
next = generateNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Blocking IPs', () => {
|
||||||
|
it('is disabled when the env var is not defined', () => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(undefined);
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is disabled when the env var is an empty string', () => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('');
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is disabled when the env var contains comma separated empty strings', () => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(' , , ');
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when the ip does not match', () => {
|
||||||
|
req.ip = '192.168.1.1';
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2');
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when the blocker IP does not match', async () => {
|
||||||
|
req.ip = '192.168.1.1';
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: '192.168.1.2' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when a client is blocked', async () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: '192.168.1.1' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the blocker IP is blocked', async () => {
|
||||||
|
req.ip = '192.168.1.1';
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: '192.168.1.1' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkIPBlockedErrorThrown(next);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Blocking clients', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('');
|
||||||
|
req.headers['x-client'] = 'test-client';
|
||||||
|
});
|
||||||
|
it('is disabled when no clients are blocked', () => {
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when the client does not match', async () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'another-client' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the client is blocked', async () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'test-client' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkClientBlockedErrorThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when an ip is blocked', async () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: 'test-client' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the list when data changes', async () => {
|
||||||
|
let blockCallback;
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
blockCallback = callback;
|
||||||
|
if (event === 'change') {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'another-client' } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
checkErrorNotThrown(next);
|
||||||
|
blockCallback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'test-client' } });
|
||||||
|
attachBlocker(req, res, next);
|
||||||
|
expect(next).to.have.been.calledTwice;
|
||||||
|
const calledWith = next.getCall(1).args;
|
||||||
|
expect(calledWith[0].message).to.equal(apiError('clientBlocked'));
|
||||||
|
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import { v4 as generateUUID } from 'uuid';
|
|
||||||
import {
|
|
||||||
generateRes,
|
|
||||||
generateReq,
|
|
||||||
generateTodo,
|
|
||||||
generateDaily,
|
|
||||||
} from '../../../helpers/api-unit.helper';
|
|
||||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
|
||||||
import { model as User } from '../../../../website/server/models/user';
|
|
||||||
import { model as Group } from '../../../../website/server/models/group';
|
|
||||||
import * as Tasks from '../../../../website/server/models/task';
|
|
||||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
|
||||||
import * as cronLib from '../../../../website/server/libs/cron';
|
|
||||||
|
|
||||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
|
||||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
|
||||||
|
|
||||||
describe('cron middleware', () => {
|
|
||||||
let res; let
|
|
||||||
req;
|
|
||||||
let user;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
res = generateRes();
|
|
||||||
req = generateReq();
|
|
||||||
user = await res.locals.user.save();
|
|
||||||
res.analytics = analyticsService;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next when user is not attached', done => {
|
|
||||||
res.locals.user = null;
|
|
||||||
cronMiddleware(req, res, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next when days have not been missed', done => {
|
|
||||||
cronMiddleware(req, res, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear todos older than 30 days for free users', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.not.exist;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.exist;
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.not.exist;
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call next if user was not modified after cron', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(hpBefore).to.equal(user.stats.hp);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runs cron if previous cron was incomplete', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
|
||||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
|
||||||
const now = new Date();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does damage for missing dailies', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const daily = generateDaily(user);
|
|
||||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await daily.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
|
||||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates tasks', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const todo = generateTodo(user);
|
|
||||||
const todoValueBefore = todo.value;
|
|
||||||
await Promise.all([todo.save(), user.save()]);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
|
||||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies quest progress', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const daily = generateDaily(user);
|
|
||||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await daily.save();
|
|
||||||
|
|
||||||
const questKey = 'dilatory';
|
|
||||||
user.party.quest.key = questKey;
|
|
||||||
|
|
||||||
const party = new Group({
|
|
||||||
type: 'party',
|
|
||||||
name: generateUUID(),
|
|
||||||
leader: user._id,
|
|
||||||
});
|
|
||||||
party.quest.members[user._id] = true;
|
|
||||||
party.quest.key = questKey;
|
|
||||||
await party.save();
|
|
||||||
|
|
||||||
user.party._id = party._id;
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
party.startQuest(user);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
|
||||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
const updatedUser = user.toObject();
|
|
||||||
updatedUser.matchedCount = 0;
|
|
||||||
|
|
||||||
sandbox.spy(cronLib, 'recoverCron');
|
|
||||||
|
|
||||||
sandbox.stub(User, 'updateOne')
|
|
||||||
.withArgs({
|
|
||||||
_id: user._id,
|
|
||||||
$or: [
|
|
||||||
{ _cronSignature: 'NOT_RUNNING' },
|
|
||||||
{ _cronSignature: { $lt: sinon.match.number } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.returns({
|
|
||||||
exec () {
|
|
||||||
return Promise.resolve(updatedUser);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
|
||||||
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cronSignature less than an hour ago should error', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await User.updateOne({
|
|
||||||
_id: user._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
|
||||||
},
|
|
||||||
}).exec();
|
|
||||||
await user.save();
|
|
||||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (!err) return reject(new Error('Cron should have failed.'));
|
|
||||||
expect(err.message).to.be.equal(expectedErrMessage);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await User.updateOne({
|
|
||||||
_id: user._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
|
||||||
},
|
|
||||||
}).exec();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cron should not run more than once', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
sandbox.spy(cronLib, 'cron');
|
|
||||||
|
|
||||||
await Promise.all([new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}), new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}), new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}, 400);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(cronLib.cron).to.be.calledOnce;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import nconf from 'nconf';
|
|
||||||
import requireAgain from 'require-again';
|
|
||||||
import {
|
|
||||||
generateRes,
|
|
||||||
generateReq,
|
|
||||||
generateNext,
|
|
||||||
} from '../../../helpers/api-unit.helper';
|
|
||||||
import { Forbidden } from '../../../../website/server/libs/errors';
|
|
||||||
import { apiError } from '../../../../website/server/libs/apiError';
|
|
||||||
|
|
||||||
function checkErrorThrown (next) {
|
|
||||||
expect(next).to.have.been.calledOnce;
|
|
||||||
const calledWith = next.getCall(0).args;
|
|
||||||
expect(calledWith[0].message).to.equal(apiError('ipAddressBlocked'));
|
|
||||||
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkErrorNotThrown (next) {
|
|
||||||
expect(next).to.have.been.calledOnce;
|
|
||||||
const calledWith = next.getCall(0).args;
|
|
||||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ipBlocker middleware', () => {
|
|
||||||
const pathToIpBlocker = '../../../../website/server/middlewares/ipBlocker';
|
|
||||||
|
|
||||||
let res; let req; let next;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
res = generateRes();
|
|
||||||
req = generateReq();
|
|
||||||
next = generateNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is disabled when the env var is not defined', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(undefined);
|
|
||||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
|
||||||
attachIpBlocker(req, res, next);
|
|
||||||
|
|
||||||
checkErrorNotThrown(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is disabled when the env var is an empty string', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('');
|
|
||||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
|
||||||
attachIpBlocker(req, res, next);
|
|
||||||
|
|
||||||
checkErrorNotThrown(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is disabled when the env var contains comma separated empty strings', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(' , , ');
|
|
||||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
|
||||||
attachIpBlocker(req, res, next);
|
|
||||||
|
|
||||||
checkErrorNotThrown(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not throw when the ip does not match', () => {
|
|
||||||
req.ip = '192.168.1.1';
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2');
|
|
||||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
|
||||||
attachIpBlocker(req, res, next);
|
|
||||||
|
|
||||||
checkErrorNotThrown(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when the ip is blocked', () => {
|
|
||||||
req.ip = '192.168.1.1';
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
|
|
||||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
|
||||||
attachIpBlocker(req, res, next);
|
|
||||||
|
|
||||||
checkErrorThrown(next);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
import { model as User } from '../../../../website/server/models/user';
|
import { model as User } from '../../../../website/server/models/user';
|
||||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||||
import { model as Group } from '../../../../website/server/models/group';
|
import { model as Group } from '../../../../website/server/models/group';
|
||||||
|
import { model as Blocker } from '../../../../website/server/models/blocker';
|
||||||
import common from '../../../../website/common';
|
import common from '../../../../website/common';
|
||||||
|
|
||||||
|
const pathToUserSchema = '../../../../website/server/models/user/schema';
|
||||||
|
|
||||||
describe('User Model', () => {
|
describe('User Model', () => {
|
||||||
describe('.toJSON()', () => {
|
describe('.toJSON()', () => {
|
||||||
it('keeps user._tmp when calling .toJSON', () => {
|
it('keeps user._tmp when calling .toJSON', () => {
|
||||||
@@ -912,4 +916,73 @@ describe('User Model', () => {
|
|||||||
expect(user.toJSON().flags.newStuff).to.equal(true);
|
expect(user.toJSON().flags.newStuff).to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('validates email', () => {
|
||||||
|
it('does not throw an error for a valid email', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.auth.local.email = 'hello@example.com';
|
||||||
|
const errors = user.validateSync();
|
||||||
|
expect(errors.errors['auth.local.email']).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if email is not valid', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.auth.local.email = 'invalid-email';
|
||||||
|
const errors = user.validateSync();
|
||||||
|
expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmail'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if email is using a restricted domain', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.auth.local.email = 'scammer@habitica.com';
|
||||||
|
const errors = user.validateSync();
|
||||||
|
expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmailDomain', { domains: 'habitica.com, habitrpg.com' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if email was blocked specifically', () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@example.com' } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||||
|
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||||
|
expect(valid).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if email domain was blocked', () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||||
|
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||||
|
expect(valid).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if user portion of email was blocked', () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||||
|
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||||
|
expect(valid).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw an error if email is not blocked', () => {
|
||||||
|
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||||
|
on: (event, callback) => {
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } });
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } });
|
||||||
|
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'bad@test.com' } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||||
|
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('good@test.com'));
|
||||||
|
expect(valid).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
requester,
|
requester,
|
||||||
translate as t,
|
translate as t,
|
||||||
|
generateUser,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import i18n from '../../../../../website/common/script/i18n';
|
import i18n from '../../../../../website/common/script/i18n';
|
||||||
|
|
||||||
@@ -56,4 +57,28 @@ describe('GET /content', () => {
|
|||||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||||
expect(res).to.not.have.property('backgroundsFlat');
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('authenticated user', () => {
|
||||||
|
let user;
|
||||||
|
it('returns content in user\'s preferred language when no language parameter is provided', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'de' });
|
||||||
|
const res = await user.get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects language parameter over user\'s preferred language', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'de' });
|
||||||
|
const res = await user.get('/content?language=fr');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to English if user\'s preferred language is invalid', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'invalid_lang' });
|
||||||
|
const res = await user.get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe('POST /debug/jump-time', () => {
|
|||||||
expect(resultDate.getDate()).to.eql(today.getDate());
|
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
|
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 365 })).time);
|
||||||
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
|
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||||
|
|
||||||
// test response values
|
// test response values
|
||||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||||
expect(heroRes.contributor.level).to.equal(1);
|
expect(heroRes.contributor.level).to.equal(1);
|
||||||
expect(heroRes.purchased.ads).to.equal(true);
|
expect(heroRes.purchased.ads).to.equal(true);
|
||||||
// test hero values
|
// test hero values
|
||||||
await hero.sync();
|
await hero.sync();
|
||||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||||
expect(hero.contributor.level).to.equal(1);
|
expect(hero.contributor.level).to.equal(1);
|
||||||
expect(hero.purchased.ads).to.equal(true);
|
expect(hero.purchased.ads).to.equal(true);
|
||||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||||
@@ -137,12 +137,12 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||||
|
|
||||||
// test response values
|
// test response values
|
||||||
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
|
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||||
expect(heroRes.contributor.level).to.equal(6);
|
expect(heroRes.contributor.level).to.equal(6);
|
||||||
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
||||||
// test hero values
|
// test hero values
|
||||||
await hero.sync();
|
await hero.sync();
|
||||||
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
|
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||||
expect(hero.contributor.level).to.equal(6);
|
expect(hero.contributor.level).to.equal(6);
|
||||||
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
||||||
});
|
});
|
||||||
|
|||||||
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
import common from '../../../../../website/common';
|
||||||
|
|
||||||
|
describe('GET /members/username/:username', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates req.params.username', async () => {
|
||||||
|
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a member\'s public data only', async () => {
|
||||||
|
// make sure user has all the fields that can be returned by the getMember call
|
||||||
|
const member = await generateUser({
|
||||||
|
contributor: { level: 1 },
|
||||||
|
backer: { tier: 3 },
|
||||||
|
preferences: {
|
||||||
|
costume: false,
|
||||||
|
background: 'volcano',
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
text: 'Clark Kent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
|
||||||
|
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||||
|
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||||
|
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||||
|
]);
|
||||||
|
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||||
|
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||||
|
'size', 'hair', 'skin', 'shirt',
|
||||||
|
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||||
|
].sort());
|
||||||
|
|
||||||
|
expect(memberRes.stats.maxMP).to.exist;
|
||||||
|
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||||
|
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||||
|
expect(memberRes.inbox.optOut).to.exist;
|
||||||
|
expect(memberRes.inbox.canReceive).to.exist;
|
||||||
|
expect(memberRes.inbox.messages).to.not.exist;
|
||||||
|
expect(memberRes.secret).to.not.exist;
|
||||||
|
|
||||||
|
expect(memberRes.blocks).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -101,34 +101,6 @@ describe('GET /tasks/user', () => {
|
|||||||
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
|
|
||||||
const LIMIT = 30;
|
|
||||||
const numberOfTodos = LIMIT + 1;
|
|
||||||
const todosInput = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
|
||||||
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
|
|
||||||
}
|
|
||||||
const todos = await user.post('/tasks/user', todosInput);
|
|
||||||
await user.sync();
|
|
||||||
const initialTodoCount = user.tasksOrder.todos.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
|
||||||
const id = todos[i]._id;
|
|
||||||
|
|
||||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
|
||||||
}
|
|
||||||
await user.sync();
|
|
||||||
|
|
||||||
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
|
|
||||||
|
|
||||||
const completedTodos = await user.get('/tasks/user?type=completedTodos');
|
|
||||||
expect(completedTodos.length).to.equal(LIMIT);
|
|
||||||
|
|
||||||
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
|
|
||||||
expect(allCompletedTodos.length).to.equal(numberOfTodos);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns dailies with isDue for the date specified', async () => {
|
it('returns dailies with isDue for the date specified', async () => {
|
||||||
// @TODO Add required format
|
// @TODO Add required format
|
||||||
const startDate = moment().subtract('1', 'days').toISOString();
|
const startDate = moment().subtract('1', 'days').toISOString();
|
||||||
|
|||||||
@@ -125,6 +125,90 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
expect(body.finalLvl).to.eql(user.stats.lvl);
|
expect(body.finalLvl).to.eql(user.stats.lvl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('handles drops', async () => {
|
||||||
|
let randomStub;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
randomStub.restore();
|
||||||
|
});
|
||||||
|
it('gives user a drop', async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
'stats.gp': 100,
|
||||||
|
'achievements.completedTask': true,
|
||||||
|
'items.eggs': {
|
||||||
|
Wolf: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||||
|
const task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||||
|
expect(res._tmp.drop).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not give a drop when non-sub drop cap is reached', async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
'stats.gp': 100,
|
||||||
|
'achievements.completedTask': true,
|
||||||
|
'items.eggs': {
|
||||||
|
Wolf: 1,
|
||||||
|
},
|
||||||
|
'items.lastDrop.count': 5,
|
||||||
|
});
|
||||||
|
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||||
|
const task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||||
|
expect(res._tmp.drop).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives a drop when subscriber is over regular cap but under subscriber cap', async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
'stats.gp': 100,
|
||||||
|
'achievements.completedTask': true,
|
||||||
|
'items.eggs': {
|
||||||
|
Wolf: 1,
|
||||||
|
},
|
||||||
|
'items.lastDrop.count': 6,
|
||||||
|
'purchased.plan.customerId': '123',
|
||||||
|
});
|
||||||
|
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||||
|
const task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||||
|
expect(res._tmp.drop).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not give a drop when subscriber is at subscriber drop cap', async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
'stats.gp': 100,
|
||||||
|
'achievements.completedTask': true,
|
||||||
|
'items.eggs': {
|
||||||
|
Wolf: 1,
|
||||||
|
},
|
||||||
|
'items.lastDrop.count': 10,
|
||||||
|
'purchased.plan.customerId': '123',
|
||||||
|
});
|
||||||
|
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||||
|
const task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||||
|
expect(res._tmp.drop).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('todos', () => {
|
context('todos', () => {
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
|||||||
|
|
||||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||||
|
|
||||||
expect(member.notifications.length).to.equal(2);
|
const lastNotification = member.notifications[member.notifications.length - 1];
|
||||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
|
expect(lastNotification.type).to.equal('GROUP_TASK_ASSIGNED');
|
||||||
expect(member.notifications[1].taskId).to.equal(groupTask._id);
|
expect(lastNotification.taskId).to.equal(groupTask._id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('assigns a task to multiple users', async () => {
|
it('assigns a task to multiple users', async () => {
|
||||||
|
|||||||
@@ -89,10 +89,12 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('removes task assignment notification from unassigned user', async () => {
|
it('removes task assignment notification from unassigned user', async () => {
|
||||||
|
await member.sync();
|
||||||
|
const oldNotificationCount = member.notifications.length;
|
||||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||||
|
|
||||||
await member.sync();
|
await member.sync();
|
||||||
expect(member.notifications.length).to.equal(1); // mystery items
|
expect(member.notifications.length).to.equal(oldNotificationCount - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unassigns a user and only that user from a task', async () => {
|
it('unassigns a user and only that user from a task', async () => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe('GET /user/auth/apple', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('registers a new user', async () => {
|
it('registers a new user', async () => {
|
||||||
const response = await api.get(appleEndpoint);
|
const response = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||||
|
|
||||||
expect(response.apiToken).to.exist;
|
expect(response.apiToken).to.exist;
|
||||||
expect(response.id).to.exist;
|
expect(response.id).to.exist;
|
||||||
@@ -35,7 +35,7 @@ describe('GET /user/auth/apple', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('logs an existing user in', async () => {
|
it('logs an existing user in', async () => {
|
||||||
const registerResponse = await api.get(appleEndpoint);
|
const registerResponse = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||||
|
|
||||||
const response = await api.get(appleEndpoint);
|
const response = await api.get(appleEndpoint);
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
|||||||
expect(isPassValid).to.equal(true);
|
expect(isPassValid).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('changes the apiToken on password reset', async () => {
|
||||||
|
const user = await generateUser();
|
||||||
|
const previousToken = user.apiToken;
|
||||||
|
|
||||||
|
const code = encrypt(JSON.stringify({
|
||||||
|
userId: user._id,
|
||||||
|
expiresAt: moment().add({ days: 1 }),
|
||||||
|
}));
|
||||||
|
await user.updateOne({
|
||||||
|
'auth.local.passwordResetCode': code,
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.post(`${endpoint}`, {
|
||||||
|
newPassword: 'my new password',
|
||||||
|
confirmPassword: 'my new password',
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
expect(user.apiToken).to.not.eql(previousToken);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||||
const user = await generateUser();
|
const user = await generateUser();
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ describe('POST /user/auth/local/login', () => {
|
|||||||
expect(isValidPassword).to.equal(true);
|
expect(isValidPassword).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets auth.timestamps.updated', async () => {
|
||||||
|
const oldUpdated = new Date(user.auth.timestamps.updated);
|
||||||
|
// login
|
||||||
|
await api.post(endpoint, {
|
||||||
|
username: user.auth.local.email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||||
|
});
|
||||||
|
|
||||||
it('user uses social authentication and has no password', async () => {
|
it('user uses social authentication and has no password', async () => {
|
||||||
await user.unset({
|
await user.unset({
|
||||||
'auth.local.hashed_password': 1,
|
'auth.local.hashed_password': 1,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
translate as t,
|
translate as t,
|
||||||
getProperty,
|
getProperty,
|
||||||
} from '../../../../../helpers/api-integration/v3';
|
} from '../../../../../helpers/api-integration/v3';
|
||||||
|
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||||
|
|
||||||
describe('POST /user/auth/social', () => {
|
describe('POST /user/auth/social', () => {
|
||||||
let api;
|
let api;
|
||||||
@@ -64,6 +65,18 @@ describe('POST /user/auth/social', () => {
|
|||||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fails if allowRegister is false and user does not exist', async () => {
|
||||||
|
await expect(api.post(endpoint, {
|
||||||
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
|
network,
|
||||||
|
allowRegister: false,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: `${apiErrorMessages.socialFlowUserNotFound} ${user.auth.local.username}+google@example.com`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('logs an existing user in', async () => {
|
it('logs an existing user in', async () => {
|
||||||
const registerResponse = await api.post(endpoint, {
|
const registerResponse = await api.post(endpoint, {
|
||||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
@@ -131,6 +144,36 @@ describe('POST /user/auth/social', () => {
|
|||||||
expect(response.newUser).to.be.false;
|
expect(response.newUser).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('logs an existing user into their social account if allowRegister is false', async () => {
|
||||||
|
const registerResponse = await api.post(endpoint, {
|
||||||
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
|
network,
|
||||||
|
});
|
||||||
|
expect(registerResponse.newUser).to.be.true;
|
||||||
|
// This is important for existing accounts before the new social handling
|
||||||
|
passport._strategies.google.userProfile.restore();
|
||||||
|
const expectedResult = {
|
||||||
|
id: randomGoogleId,
|
||||||
|
displayName: 'a google user',
|
||||||
|
emails: [
|
||||||
|
{ value: user.auth.local.email },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||||
|
|
||||||
|
const response = await api.post(endpoint, {
|
||||||
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
|
network,
|
||||||
|
allowRegister: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||||
|
expect(response.id).to.eql(registerResponse.id);
|
||||||
|
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||||
|
expect(response.id).not.to.eql(user._id);
|
||||||
|
expect(response.newUser).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
it('add social auth to an existing user', async () => {
|
it('add social auth to an existing user', async () => {
|
||||||
const response = await user.post(endpoint, {
|
const response = await user.post(endpoint, {
|
||||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
@@ -167,5 +210,24 @@ describe('POST /user/auth/social', () => {
|
|||||||
|
|
||||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets auth.timestamps.updated', async () => {
|
||||||
|
let oldUpdated = new Date(user.auth.timestamps.updated);
|
||||||
|
await user.post(endpoint, {
|
||||||
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
|
network,
|
||||||
|
});
|
||||||
|
await user.sync();
|
||||||
|
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||||
|
oldUpdated = new Date(user.auth.timestamps.updated);
|
||||||
|
|
||||||
|
// Do it again to ensure it updates even when nothing else changes
|
||||||
|
await api.post(endpoint, {
|
||||||
|
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||||
|
network,
|
||||||
|
});
|
||||||
|
await user.sync();
|
||||||
|
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,11 +27,30 @@ describe('PUT /user/auth/update-password', async () => {
|
|||||||
newPassword,
|
newPassword,
|
||||||
confirmPassword: newPassword,
|
confirmPassword: newPassword,
|
||||||
});
|
});
|
||||||
expect(response).to.eql({});
|
|
||||||
|
expect(response).to.exist;
|
||||||
|
expect(response.apiToken).to.exist;
|
||||||
|
|
||||||
await user.sync();
|
await user.sync();
|
||||||
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should change the apiToken on password change', async () => {
|
||||||
|
const previousToken = user.apiToken;
|
||||||
|
const response = await user.put(ENDPOINT, {
|
||||||
|
password,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword: newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newToken = response.apiToken;
|
||||||
|
expect(newToken).to.exist;
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
expect(user.apiToken).to.eql(newToken);
|
||||||
|
expect(user.apiToken).to.not.eql(previousToken);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an error when confirmPassword does not match newPassword', async () => {
|
it('returns an error when confirmPassword does not match newPassword', async () => {
|
||||||
await expect(user.put(ENDPOINT, {
|
await expect(user.put(ENDPOINT, {
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('POST /user/buy-mystery-set/:key', () => {
|
|||||||
|
|
||||||
expect(res.data).to.eql({
|
expect(res.data).to.eql({
|
||||||
items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
|
items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
|
||||||
purchasedPlanConsecutive: user.purchased.plan.consecutive,
|
purchasedPlanConsecutive: JSON.parse(JSON.stringify(user.purchased.plan.consecutive)),
|
||||||
});
|
});
|
||||||
expect(res.message).to.equal(t('hourglassPurchaseSet'));
|
expect(res.message).to.equal(t('hourglassPurchaseSet'));
|
||||||
});
|
});
|
||||||
|
|||||||
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import find from 'lodash/find';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the messages array if the uniqueMessageId has the like flag
|
||||||
|
* @param {InboxMessage[]} messages
|
||||||
|
* @param {String} uniqueMessageId
|
||||||
|
* @param {String} userId
|
||||||
|
* @param {Boolean} likeStatus
|
||||||
|
*/
|
||||||
|
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
|
||||||
|
const messageToCheck = find(messages, { uniqueMessageId });
|
||||||
|
|
||||||
|
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line mocha/no-exclusive-tests
|
||||||
|
describe('POST /inbox/like-private-message/:messageId', () => {
|
||||||
|
let userToSendMessage;
|
||||||
|
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
userToSendMessage = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when private message is not found', async () => {
|
||||||
|
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('messageGroupChatNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('likes a message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||||
|
|
||||||
|
const senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
|
||||||
|
|
||||||
|
const receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows a user to like their own private message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
|
||||||
|
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
|
||||||
|
|
||||||
|
const messages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
|
||||||
|
|
||||||
|
const receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlikes a message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
|
||||||
|
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||||
|
|
||||||
|
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
|
||||||
|
expect(unlikeResult.likes[receiver._id]).to.equal(false);
|
||||||
|
|
||||||
|
const messages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
|
||||||
|
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
|
||||||
|
expect(messageToCheck.likes[receiver._id]).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,6 +40,24 @@ describe('GET /user', () => {
|
|||||||
expect(returnedUser.stats).to.not.exist;
|
expect(returnedUser.stats).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns when ALWAYS_LOADED paths are requested', async () => {
|
||||||
|
const returnedUser = await user.get('/user?userFields=_id,notifications,preferences,auth,flags,permissions');
|
||||||
|
|
||||||
|
expect(returnedUser._id).to.equal(user._id);
|
||||||
|
expect(returnedUser.notifications).to.exist;
|
||||||
|
expect(returnedUser.preferences).to.exist;
|
||||||
|
expect(returnedUser.auth).to.exist;
|
||||||
|
expect(returnedUser.flags).to.exist;
|
||||||
|
expect(returnedUser.permissions).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when subpaths paths are requested', async () => {
|
||||||
|
const returnedUser = await user.get('/user?userFields=auth.local.username');
|
||||||
|
|
||||||
|
expect(returnedUser._id).to.equal(user._id);
|
||||||
|
expect(returnedUser.auth.local.username).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
it('does not return requested private properties', async () => {
|
it('does not return requested private properties', async () => {
|
||||||
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
||||||
|
|
||||||
|
|||||||
66
test/api/v4/user/auth/POST-check_email.test.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
translate as t,
|
||||||
|
requester,
|
||||||
|
generateUser,
|
||||||
|
} from '../../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
const ENDPOINT = '/user/auth/check-email';
|
||||||
|
|
||||||
|
describe('POST /user/auth/check-email', () => {
|
||||||
|
const email = 'SOmE-nEw-emAIl_2@example.net';
|
||||||
|
let api;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
api = requester();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns email if it is not used yet', async () => {
|
||||||
|
const response = await api.post(ENDPOINT, {
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
expect(response.email).to.eql(email);
|
||||||
|
expect(response.valid).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if email is not provided', async () => {
|
||||||
|
await expect(api.post(ENDPOINT, {
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if email is already taken', async () => {
|
||||||
|
const user = await generateUser();
|
||||||
|
|
||||||
|
const response = await api.post(ENDPOINT, {
|
||||||
|
email: user.auth.local.email,
|
||||||
|
});
|
||||||
|
expect(response).to.eql({
|
||||||
|
valid: false,
|
||||||
|
email: user.auth.local.email,
|
||||||
|
error: t('cannotFulfillReq'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if casing is different', async () => {
|
||||||
|
const user = await generateUser();
|
||||||
|
|
||||||
|
const response = await api.post(ENDPOINT, {
|
||||||
|
email: user.auth.local.email.toUpperCase(),
|
||||||
|
});
|
||||||
|
expect(response).to.eql({
|
||||||
|
valid: false,
|
||||||
|
email: user.auth.local.email.toUpperCase(),
|
||||||
|
error: t('cannotFulfillReq'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if email uses restricted domain', async () => {
|
||||||
|
const response = await api.post(ENDPOINT, {
|
||||||
|
email: 'fake@habitica.com',
|
||||||
|
});
|
||||||
|
expect(response.valid).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -183,8 +183,6 @@ describe('cron utility functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getPlanContext', () => {
|
describe('getPlanContext', () => {
|
||||||
const now = new Date(2022, 5, 1);
|
|
||||||
|
|
||||||
function baseUserData (count, offset, planId) {
|
function baseUserData (count, offset, planId) {
|
||||||
return {
|
return {
|
||||||
purchased: {
|
purchased: {
|
||||||
@@ -192,7 +190,7 @@ describe('cron utility functions', () => {
|
|||||||
consecutive: {
|
consecutive: {
|
||||||
count,
|
count,
|
||||||
offset,
|
offset,
|
||||||
gemCapExtra: 25,
|
gemCapExtra: 26,
|
||||||
trinkets: 19,
|
trinkets: 19,
|
||||||
},
|
},
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
@@ -213,52 +211,19 @@ describe('cron utility functions', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('monthly plan, next date in 3 months', () => {
|
it('elapsedMonths is 0 if its the same month', () => {
|
||||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||||
user.purchased.plan.perkMonthCount = 0;
|
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
const planContext = getPlanContext(user, new Date(2022, 4, 20));
|
||||||
|
expect(planContext.elapsedMonths).to.equal(0);
|
||||||
expect(planContext.nextHourglassDate)
|
|
||||||
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('monthly plan, next date in 1 month', () => {
|
it('elapsedMonths is 1 after one month', () => {
|
||||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||||
user.purchased.plan.perkMonthCount = 2;
|
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
const planContext = getPlanContext(user, new Date(2022, 5, 11));
|
||||||
|
|
||||||
expect(planContext.nextHourglassDate)
|
expect(planContext.elapsedMonths).to.equal(1);
|
||||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multi-month plan, no offset', () => {
|
|
||||||
const user = baseUserData(60, 0, 'basic_3mo');
|
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
|
||||||
|
|
||||||
expect(planContext.nextHourglassDate)
|
|
||||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multi-month plan with offset', () => {
|
|
||||||
const user = baseUserData(60, 1, 'basic_3mo');
|
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
|
||||||
|
|
||||||
expect(planContext.nextHourglassDate)
|
|
||||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multi-month plan with perk count', () => {
|
|
||||||
const user = baseUserData(60, 1, 'basic_3mo');
|
|
||||||
user.purchased.plan.perkMonthCount = 2;
|
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
|
||||||
|
|
||||||
expect(planContext.nextHourglassDate)
|
|
||||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty array when no events are active', () => {
|
it('returns empty array when no events are active', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-01-06'));
|
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||||
const events = getRepeatingEvents();
|
const events = getRepeatingEvents();
|
||||||
expect(events).to.be.empty;
|
expect(events).to.be.empty;
|
||||||
});
|
});
|
||||||
@@ -27,14 +27,14 @@ describe('events', () => {
|
|||||||
it('returns nye event at beginning of the year', () => {
|
it('returns nye event at beginning of the year', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2025-01-01'));
|
clock = sinon.useFakeTimers(new Date('2025-01-01'));
|
||||||
const events = getRepeatingEvents();
|
const events = getRepeatingEvents();
|
||||||
expect(events).to.have.length(1);
|
expect(events).to.have.length(2);
|
||||||
expect(events[0].key).to.equal('nye');
|
expect(events[0].key).to.equal('nye');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns nye event at end of the year', () => {
|
it('returns nye event at end of the year', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-12-30'));
|
clock = sinon.useFakeTimers(new Date('2024-12-30'));
|
||||||
const events = getRepeatingEvents();
|
const events = getRepeatingEvents();
|
||||||
expect(events).to.have.length(1);
|
expect(events).to.have.length(2);
|
||||||
expect(events[0].key).to.equal('nye');
|
expect(events[0].key).to.equal('nye');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('food', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets canDrop for pie if it is pie season', () => {
|
it('sets canDrop for pie if it is pie season', () => {
|
||||||
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
|
clock = sinon.useFakeTimers(new Date(2024, 2, 15));
|
||||||
const datedContent = require('../../website/common/script/content').default;
|
const datedContent = require('../../website/common/script/content').default;
|
||||||
each(datedContent.food, foodItem => {
|
each(datedContent.food, foodItem => {
|
||||||
if (foodItem.key.indexOf('Pie_') !== -1) {
|
if (foodItem.key.indexOf('Pie_') !== -1) {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ describe('content index', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('marks pie as buyable and droppable during pi day', () => {
|
it('marks pie as buyable and droppable during pi day', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-03-14'));
|
clock = sinon.useFakeTimers(new Date('2024-03-15'));
|
||||||
const { food } = content;
|
const { food } = content;
|
||||||
Object.keys(food).forEach(key => {
|
Object.keys(food).forEach(key => {
|
||||||
if (key === 'Saddle') {
|
if (key === 'Saddle') {
|
||||||
|
|||||||
42
test/content/quests.test.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
each,
|
||||||
|
} from 'lodash';
|
||||||
|
import {
|
||||||
|
expectValidTranslationString,
|
||||||
|
} from '../helpers/content.helper';
|
||||||
|
|
||||||
|
import { quests } from '../../website/common/script/content/quests';
|
||||||
|
|
||||||
|
describe('quests', () => {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains basic information about each quest', () => {
|
||||||
|
each(quests, (quest, key) => {
|
||||||
|
expectValidTranslationString(quest.text);
|
||||||
|
expectValidTranslationString(quest.notes);
|
||||||
|
expectValidTranslationString(quest.completion);
|
||||||
|
expect(quest.key, key).to.equal(key);
|
||||||
|
expect(quest.category, key).to.be.a('string');
|
||||||
|
if (quest.boss) {
|
||||||
|
expectValidTranslationString(quest.boss.name);
|
||||||
|
expect(quest.boss.hp, key).to.be.a('number');
|
||||||
|
expect(quest.boss.str, key).to.be.a('number');
|
||||||
|
}
|
||||||
|
expect(quest.drop).to.be.an('object');
|
||||||
|
expect(quest.drop.gp, key).to.be.a('number');
|
||||||
|
expect(quest.drop.exp, key).to.be.a('number');
|
||||||
|
if (quest.drop.items) {
|
||||||
|
quest.drop.items.forEach(drop => {
|
||||||
|
expectValidTranslationString(drop.text);
|
||||||
|
expect(drop.type, key).to.exist;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,8 +19,8 @@ describe('releaseDates', () => {
|
|||||||
});
|
});
|
||||||
describe('armoire', () => {
|
describe('armoire', () => {
|
||||||
it('should only contain valid armoire names', () => {
|
it('should only contain valid armoire names', () => {
|
||||||
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
|
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-22`));
|
||||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
|
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-22`));
|
||||||
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
|
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
|
||||||
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
|
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
|
||||||
});
|
});
|
||||||
@@ -40,8 +40,8 @@ describe('releaseDates', () => {
|
|||||||
|
|
||||||
describe('eggs', () => {
|
describe('eggs', () => {
|
||||||
it('should only contain valid egg names', () => {
|
it('should only contain valid egg names', () => {
|
||||||
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
|
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
|
||||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
|
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
|
||||||
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
|
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
|
||||||
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
|
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
|
||||||
});
|
});
|
||||||
@@ -61,8 +61,8 @@ describe('releaseDates', () => {
|
|||||||
|
|
||||||
describe('hatchingPotions', () => {
|
describe('hatchingPotions', () => {
|
||||||
it('should only contain valid potion names', () => {
|
it('should only contain valid potion names', () => {
|
||||||
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
|
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
|
||||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
|
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
|
||||||
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
|
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
|
||||||
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
|
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
import maxBy from 'lodash/maxBy';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +11,7 @@ import QUEST_BUNDLES from '../../website/common/script/content/bundles';
|
|||||||
import potions from '../../website/common/script/content/hatching-potions';
|
import potions from '../../website/common/script/content/hatching-potions';
|
||||||
import SPELLS from '../../website/common/script/content/spells';
|
import SPELLS from '../../website/common/script/content/spells';
|
||||||
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
|
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
|
||||||
|
import { HATCHING_POTIONS_RELEASE_DATES } from '../../website/common/script/content/constants/releaseDates';
|
||||||
|
|
||||||
function validateMatcher (matcher, checkedDate) {
|
function validateMatcher (matcher, checkedDate) {
|
||||||
expect(matcher.end).to.be.a('date');
|
expect(matcher.end).to.be.a('date');
|
||||||
@@ -131,15 +133,21 @@ describe('Content Schedule', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets the end date for a gala', () => {
|
it('sets the end date for a gala', () => {
|
||||||
const date = new Date('2024-05-20');
|
const date = new Date('2024-05-31');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the end date for a winter gala', () => {
|
it('sets the end date for a winter gala', () => {
|
||||||
const date = new Date('2024-12-22');
|
const date = new Date('2025-02-28');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date in new year for a winter gala', () => {
|
||||||
|
const date = new Date('2025-02-28');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses correct date for first hours of the month', () => {
|
it('uses correct date for first hours of the month', () => {
|
||||||
@@ -182,7 +190,7 @@ describe('Content Schedule', () => {
|
|||||||
const date = new Date('2024-04-15');
|
const date = new Date('2024-04-15');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.premiumHatchingPotions).to.exist;
|
expect(matchers.premiumHatchingPotions).to.exist;
|
||||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
|
||||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||||
});
|
});
|
||||||
@@ -222,6 +230,8 @@ describe('Content Schedule', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('premium hatching potions', () => {
|
it('premium hatching potions', () => {
|
||||||
|
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
|
||||||
|
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
|
||||||
const potionKeys = Object.keys(potions.premium);
|
const potionKeys = Object.keys(potions.premium);
|
||||||
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||||
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
|
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
|
||||||
@@ -262,6 +272,21 @@ describe('Content Schedule', () => {
|
|||||||
expect(matcher.match('backgroundkey072024')).to.be.true;
|
expect(matcher.match('backgroundkey072024')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows background matching the month for new backgrounds from multiple years', () => {
|
||||||
|
const date = new Date('2026-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey072024')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey072025')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey072026')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows background matching the previous month in the first week for new backgrounds', () => {
|
||||||
|
const date = new Date('2024-09-02');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey082024')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey092024')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
it('disallows background in the future', () => {
|
it('disallows background in the future', () => {
|
||||||
const date = new Date('2024-07-08');
|
const date = new Date('2024-07-08');
|
||||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
@@ -281,19 +306,26 @@ describe('Content Schedule', () => {
|
|||||||
expect(matcher.match('backgroundkey022021')).to.be.true;
|
expect(matcher.match('backgroundkey022021')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows background even yeared backgrounds in first half of year', () => {
|
it('allows even yeared backgrounds in first half of year', () => {
|
||||||
const date = new Date('2025-02-08');
|
const date = new Date('2025-02-08');
|
||||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
expect(matcher.match('backgroundkey022024')).to.be.true;
|
expect(matcher.match('backgroundkey022024')).to.be.true;
|
||||||
expect(matcher.match('backgroundkey082022')).to.be.true;
|
expect(matcher.match('backgroundkey082022')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows background odd yeared backgrounds in second half of year', () => {
|
it('allows odd yeared backgrounds in second half of year', () => {
|
||||||
const date = new Date('2024-08-08');
|
const date = new Date('2024-08-08');
|
||||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
expect(matcher.match('backgroundkey022023')).to.be.true;
|
expect(matcher.match('backgroundkey022023')).to.be.true;
|
||||||
expect(matcher.match('backgroundkey082021')).to.be.true;
|
expect(matcher.match('backgroundkey082021')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows odd yeared backgrounds in beginning of january', () => {
|
||||||
|
const date = new Date('2025-01-06');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey122024'), 'backgroundkey122024').to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey062023'), 'backgroundkey062022').to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('timeTravelers matcher', () => {
|
describe('timeTravelers matcher', () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('Shop Featured Items', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('contains the current premium hatching potions', () => {
|
it('contains the current premium hatching potions', () => {
|
||||||
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
|
clock = Sinon.useFakeTimers(new Date('2024-04-09'));
|
||||||
const items = featuredItems.market();
|
const items = featuredItems.market();
|
||||||
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
|
|||||||
sinonStubPromise(global.sinon);
|
sinonStubPromise(global.sinon);
|
||||||
global.sandbox = sinon.createSandbox();
|
global.sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||||
|
|
||||||
setupNconf('./config.json.example');
|
setupNconf('./config.json.example');
|
||||||
|
|||||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
before(done => {
|
before(done => {
|
||||||
mongoose.connection.on('open', err => {
|
mongoose.connection.once('open', async err => {
|
||||||
if (err) return done(err);
|
if (err) throw err;
|
||||||
return resetHabiticaDB()
|
await resetHabiticaDB();
|
||||||
.then(() => {
|
done();
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||||
|
|
||||||
// fix further imports of require/import syntaxes
|
// fix further imports of require/import syntaxes
|
||||||
require('@babel/register');
|
require('@babel/register');
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
es2021: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'habitrpg/lib/vue',
|
'habitrpg/lib/vue',
|
||||||
],
|
],
|
||||||
ignorePatterns: ['dist/', 'node_modules/'],
|
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
@@ -39,7 +40,4 @@ module.exports = {
|
|||||||
order: ['template', 'style', 'script'],
|
order: ['template', 'style', 'script'],
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
parserOptions: {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/* eslint-disable import/no-commonjs */
|
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset',
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-proposal-optional-chaining',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -7,23 +7,12 @@
|
|||||||
<title>Habitica - Gamify Your Life</title>
|
<title>Habitica - Gamify Your Life</title>
|
||||||
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
|
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
|
||||||
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
|
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
|
||||||
<meta name="smartbanner:title" content="Habitica">
|
|
||||||
<meta name="smartbanner:author" content="HabitRPG, Inc.">
|
|
||||||
<meta name="smartbanner:price" content="FREE">
|
|
||||||
<meta name="smartbanner:price-suffix-apple" content=" - On the App Store">
|
|
||||||
<meta name="smartbanner:price-suffix-google" content=" - In Google Play">
|
|
||||||
<meta name="smartbanner:icon-apple" content="/static/presskit/Logo/iOS.png">
|
|
||||||
<meta name="smartbanner:icon-google" content="/static/presskit/Logo/Android.png">
|
|
||||||
<meta name="smartbanner:button" content="VIEW">
|
|
||||||
<meta name="smartbanner:button-url-apple" content="https://itunes.apple.com/us/app/habitica-gamified-taskmanager/id994882113">
|
|
||||||
<meta name="smartbanner:button-url-google" content="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica">
|
|
||||||
<meta name="smartbanner:enabled-platforms" content="android,ios">
|
|
||||||
<meta name="smartbanner:hide-ttl" content="2592000000">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
||||||
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
||||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||||
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
||||||
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loading-screen">
|
<div id="loading-screen">
|
||||||
@@ -40,10 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
|
|
||||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||||
<!-- Translations -->
|
<!-- Translations -->
|
||||||
<script type='text/javascript' src='/api/v4/i18n/browser-script'></script>
|
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
12198
website/client/package-lock.json
generated
@@ -3,64 +3,64 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
"preview": "vite preview",
|
||||||
"lint": "vue-cli-service lint .",
|
"test:unit": "vitest run",
|
||||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
"test:unit:watch": "vitest watch",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix .",
|
||||||
|
"lint-no-fix": "eslint --ext .js,.vue --no-fix src",
|
||||||
"postinstall": "node ./scripts/npm-postinstall.js"
|
"postinstall": "node ./scripts/npm-postinstall.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.8",
|
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
"@vitejs/plugin-vue2": "^2.3.3",
|
||||||
"@vue/cli-plugin-router": "^5.0.8",
|
|
||||||
"@vue/cli-plugin-unit-mocha": "^5.0.8",
|
|
||||||
"@vue/cli-service": "^5.0.8",
|
|
||||||
"@vue/test-utils": "1.0.0-beta.29",
|
"@vue/test-utils": "1.0.0-beta.29",
|
||||||
"amplitude-js": "^8.21.3",
|
"amplitude-js": "^8.21.3",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
"axios-progress-bar": "^1.2.0",
|
"axios-progress-bar": "^1.2.0",
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-vue": "^2.23.1",
|
"bootstrap-vue": "^2.23.1",
|
||||||
"core-js": "^3.33.1",
|
|
||||||
"dompurify": "^3.0.3",
|
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-habitrpg": "6.2.0",
|
"eslint-config-habitrpg": "6.2.0",
|
||||||
"eslint-plugin-mocha": "5.3.0",
|
"eslint-plugin-mocha": "5.3.0",
|
||||||
"eslint-plugin-vue": "7.20.0",
|
"eslint-plugin-vue": "7.20.0",
|
||||||
|
"ga-gtag": "^1.2.0",
|
||||||
"habitica-markdown": "^3.0.0",
|
"habitica-markdown": "^3.0.0",
|
||||||
"hellojs": "^1.20.0",
|
"hellojs": "^1.20.0",
|
||||||
"intro.js": "^7.2.0",
|
"intro.js": "^7.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-locales-webpack-plugin": "^1.2.0",
|
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"sass": "^1.63.4",
|
"sass": "^1.63.4",
|
||||||
"sass-loader": "^14.1.1",
|
|
||||||
"sinon": "^17.0.1",
|
"sinon": "^17.0.1",
|
||||||
"smartbanner.js": "^1.19.3",
|
|
||||||
"stopword": "^2.0.8",
|
"stopword": "^2.0.8",
|
||||||
"timers-browserify": "^2.0.12",
|
"timers-browserify": "^2.0.12",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"validator": "^13.9.0",
|
"validator": "^13.9.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-compression2": "^1.3.3",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
"vue-fragment": "^1.6.0",
|
"vue-fragment": "^1.6.0",
|
||||||
"vue-mugen-scroll": "^0.2.6",
|
"vue-mugen-scroll": "^0.2.6",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vue-template-babel-compiler": "^2.0.0",
|
|
||||||
"vue-template-compiler": "^2.7.10",
|
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
|
"@vitest/browser": "^3.0.5",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
"chai": "^5.1.0",
|
|
||||||
"inspectpack": "^4.7.1",
|
"inspectpack": "^4.7.1",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"mocha": "^11.1.0",
|
||||||
|
"playwright": "^1.50.1",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"webpack": "^5.89.0"
|
"vitest": "^3.0.5",
|
||||||
|
"webpack": "^5.94.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<snackbars />
|
<snackbars />
|
||||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||||
<user-main v-else />
|
<div v-else>
|
||||||
|
<user-main />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#loading-screen-inapp {
|
#loading-screen-inapp {
|
||||||
#melior {
|
#melior {
|
||||||
@@ -90,7 +92,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang='scss'>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
opacity: .9 !important;
|
opacity: .9 !important;
|
||||||
@@ -106,18 +108,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import * as Analytics from '@/libs/analytics';
|
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import userMain from '@/pages/user-main';
|
|
||||||
import snackbars from '@/components/snackbars/notifications';
|
import snackbars from '@/components/snackbars/notifications';
|
||||||
|
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
snackbars,
|
snackbars,
|
||||||
userMain,
|
userMain: () => import('@/pages/user-main'),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -148,10 +149,6 @@ export default {
|
|||||||
this.hideLoadingScreen();
|
this.hideLoadingScreen();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$nextTick(() => {
|
|
||||||
// Load external scripts after the app has been rendered
|
|
||||||
Analytics.load();
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||||
// Verify that the user was not updated from another browser/app/client
|
// Verify that the user was not updated from another browser/app/client
|
||||||
@@ -210,22 +207,33 @@ export default {
|
|||||||
const isBanned = this.checkForBannedUser(error);
|
const isBanned = this.checkForBannedUser(error);
|
||||||
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
||||||
|
|
||||||
// Don't show errors from getting user details. These users have delete their account,
|
// Don't show errors from getting user details. These users have deleted their account,
|
||||||
// but their chat message still exists.
|
// but their chat message still exists.
|
||||||
const configExists = Boolean(error.response) && Boolean(error.response.config);
|
const configExists = Boolean(error.response) && Boolean(error.response.config);
|
||||||
if (configExists && error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
if (configExists) {
|
||||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
if (error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||||
// Chat paging should help this, but maybe we can also find another solution..
|
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||||
return Promise.resolve(error);
|
// Chat paging should help this, but maybe we can also find another solution..
|
||||||
|
return Promise.resolve(error);
|
||||||
|
}
|
||||||
|
// Also, a 404 occurs during routine attempt to log in with social,
|
||||||
|
// when we check for account already existing.
|
||||||
|
if (error.response.config.method === 'post' && (error.response.config.url.indexOf('/api/v4/user/auth/social') !== -1
|
||||||
|
|| error.response.config.url.indexOf('/api/v4/user/auth/apple') !== -1)) {
|
||||||
|
const socialEmail = error.response.data.message.split(': ')[1];
|
||||||
|
if (socialEmail) {
|
||||||
|
window.sessionStorage.setItem('social-email', socialEmail);
|
||||||
|
}
|
||||||
|
return Promise.resolve(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorData = error.response.data;
|
const errorData = error.response.data;
|
||||||
const errorMessage = errorData.message || errorData;
|
const errorMessage = errorData.message || errorData;
|
||||||
|
const errorCode = errorData.error;
|
||||||
|
|
||||||
// Check for conditions to reset the user auth
|
// If 'invalid_credentials' signaled, force logout
|
||||||
// TODO use a specific error like NotificationNotFound instead of checking for the string
|
if (error.response.status === 401 && errorCode === 'invalid_credentials') {
|
||||||
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
|
|
||||||
if (invalidUserMessage.indexOf(errorMessage) !== -1) {
|
|
||||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -268,16 +276,29 @@ export default {
|
|||||||
const loadingScreen = document.getElementById('loading-screen');
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||||
|
|
||||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
// Check if we need to show password change success message
|
||||||
this.hideLoadingScreen();
|
if (sessionStorage.getItem('passwordChangeSuccess') === 'true') {
|
||||||
|
sessionStorage.removeItem('passwordChangeSuccess');
|
||||||
|
this.$store.dispatch('snackbars:add', {
|
||||||
|
title: 'Habitica',
|
||||||
|
text: this.$t('passwordSuccess'),
|
||||||
|
type: 'success',
|
||||||
|
timeout: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$router.onReady(() => {
|
||||||
|
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||||
|
this.hideLoadingScreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hideLoadingScreen () {
|
hideLoadingScreen () {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
checkForBannedUser (error) {
|
checkForBannedUser (error) {
|
||||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||||
const errorMessage = error.response.data.message;
|
const errorMessage = error.response.data.message;
|
||||||
|
|
||||||
@@ -301,5 +322,3 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
|
||||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
height: 219px;
|
height: 219px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
|
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||||
|
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 68px;
|
height: 68px;
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,10 @@
|
|||||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Pet_HatchingPotion_Cryptid {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.Gems {
|
.Gems {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
margin-right:5px;
|
margin-right:5px;
|
||||||
@@ -172,7 +177,7 @@
|
|||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
|
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice, .Mount_Head_Dragon-Hydra, .Mount_Body_Dragon-Hydra {
|
||||||
width: 135px;
|
width: 135px;
|
||||||
height: 135px;
|
height: 135px;
|
||||||
}
|
}
|
||||||
@@ -185,6 +190,14 @@
|
|||||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Mount_Head_Dragon-Hydra {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Mount_Body_Dragon-Hydra {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.background_airship, .background_clocktower, .background_steamworks {
|
.background_airship, .background_clocktower, .background_steamworks {
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.1 KiB |
BIN
website/client/src/assets/images/confetti.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
website/client/src/assets/images/group-plans-static/group-management@3x.png
Executable file → Normal file
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 26 KiB |
BIN
website/client/src/assets/images/group-plans-static/team-based@3x.png
Executable file → Normal file
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 410 B |
BIN
website/client/src/assets/images/home/signup-quill@2x.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -19,7 +19,7 @@
|
|||||||
top: -16px !important;
|
top: -16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||||
|
|
||||||
@each $foolPet in $foolPets {
|
@each $foolPet in $foolPets {
|
||||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.featured-label {
|
.featured-label {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|||||||