Compare commits
304 Commits
v5.29.0
...
phillip/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb58e4470 | ||
|
|
c3ef26b2f3 | ||
|
|
108bd59296 | ||
|
|
5228ed40d1 | ||
|
|
becb6e49f0 | ||
|
|
aa8f0f0c4e | ||
|
|
d1891f4c43 | ||
|
|
95494c685b | ||
|
|
10978d46ab | ||
|
|
447eb6a0c4 | ||
|
|
3dec49b72c | ||
|
|
472d03f276 | ||
|
|
fd9a27c3ab | ||
|
|
a5c1423837 | ||
|
|
2381be8c46 | ||
|
|
5c7545f32a | ||
|
|
ffed5a9a97 | ||
|
|
cd58ce2233 | ||
|
|
a2b5e3621e | ||
|
|
e9829b8b60 | ||
|
|
7ecb83dc7e | ||
|
|
e8ffe2286c | ||
|
|
a06dfc9ed8 | ||
|
|
58b0e323a3 | ||
|
|
9ca60d7551 | ||
|
|
6f63583a12 | ||
|
|
fe63436a57 | ||
|
|
5b93b9b37a | ||
|
|
1d55027791 | ||
|
|
83f0984da1 | ||
|
|
53d4f75cab | ||
|
|
da45eb2adf | ||
|
|
3bf4af8d8b | ||
|
|
f030691fac | ||
|
|
d952239d35 | ||
|
|
2c7f6fd9e3 | ||
|
|
ddba450630 | ||
|
|
187238d39a | ||
|
|
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 |
@@ -7,5 +7,14 @@ module.exports = {
|
||||
rules: {
|
||||
'prefer-regex-literals': 'warn',
|
||||
'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
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'phillip/**'
|
||||
- 'sabrecat/**'
|
||||
- 'kalista/**'
|
||||
- 'natalie/**'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.gitignore
vendored
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
/mongodb-data*
|
||||
/.nyc_output
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
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",
|
||||
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
|
||||
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"BASE_URL": "http://localhost:3000",
|
||||
"BLOCKED_IPS": "",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"CRON_SAFE_MODE": "false",
|
||||
"CRON_SEMI_SAFE_MODE": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"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_USER": "user",
|
||||
"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_TEST": "false",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"FACEBOOK_KEY": "123456789012345",
|
||||
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
|
||||
@@ -29,15 +37,16 @@
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"IGNORE_REDIRECT": "true",
|
||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
@@ -55,43 +64,34 @@
|
||||
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"PORT": 3000,
|
||||
"PUSH_CONFIGS_APN_ENABLED": "false",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_TEAM_ID": "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_BUCKET": "bucket",
|
||||
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
||||
"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_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",
|
||||
"SLOW_REQUEST_THRESHOLD": 1000,
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"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",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"WEB_CONCURRENCY": 1
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ function filterFile (file) {
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
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 => {
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
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-console'); // 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-transifex-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);
|
||||
|
||||
// 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_NEW = nconf.get('MONGODB_NEW');
|
||||
|
||||
@@ -32,7 +32,7 @@ let moment = require('moment');
|
||||
consoleStamp(console);
|
||||
|
||||
// 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_NEW = nconf.get('MONGODB_NEW');
|
||||
|
||||
@@ -6,11 +6,11 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
|
||||
function setUpServer () {
|
||||
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 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();
|
||||
|
||||
// We require src/server and npt src/index because
|
||||
// We require src/server and not src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
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
|
||||
);
|
||||
|
||||
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 () {
|
||||
|
||||
@@ -11,7 +11,7 @@ const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* Award users every extant pet and mount
|
||||
* Award every extant piece of equippable gear
|
||||
*/
|
||||
|
||||
async function updateUser (user) {
|
||||
@@ -27,13 +27,13 @@ async function updateUser (user) {
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.lowerCaseUsername': 'olson1',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
@@ -57,7 +57,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.username': 'SabreTest',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
175
migrations/users/habitoween.js
Normal file
@@ -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';
|
||||
|
||||
const MIGRATION_NAME = '20181203_take_this';
|
||||
const MIGRATION_NAME = 'YYYYMMDD_take_this';
|
||||
const CHALLENGE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
@@ -41,15 +42,15 @@ async function updateUser (user) {
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
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 () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
challenges: '00708425-d477-41a5-bf27-6270466e7976',
|
||||
challenges: CHALLENGE_ID,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
13240
package-lock.json
generated
34
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.29.0",
|
||||
"version": "5.41.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -17,10 +17,10 @@
|
||||
"apple-auth": "^1.0.9",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.6.5",
|
||||
"cwait": "^1.1.1",
|
||||
@@ -28,7 +28,7 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-habitrpg": "^6.2.3",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^4.21.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"firebase-admin": "^12.1.1",
|
||||
@@ -38,11 +38,11 @@
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ip-location-api": "^4.0.0",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -50,13 +50,12 @@
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.6.3",
|
||||
"morgan": "^1.10.0",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.1",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
"nodemon": "^2.0.20",
|
||||
"on-headers": "^1.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"passport": "^0.5.3",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
@@ -72,7 +71,6 @@
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
@@ -100,27 +98,27 @@
|
||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||
"test:nodemon": "gulp test:nodemon",
|
||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"start": "node --watch ./website/server/index.js",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"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",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.8.2",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.4",
|
||||
"nyc": "^15.1.0",
|
||||
|
||||
@@ -71,15 +71,14 @@ async function deleteHabiticaData (user, email) {
|
||||
}
|
||||
|
||||
async function processEmailAddress (email) {
|
||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
||||
const localUsers = await User.find(
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ 'auth.local.email': email },
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).exec();
|
||||
|
||||
const socialUsers = await User.find(
|
||||
{
|
||||
'auth.local.email': { $not: emailRegex },
|
||||
'auth.local.email': { $ne: email },
|
||||
$or: [
|
||||
{ 'auth.facebook.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;
|
||||
|
||||
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 = [];
|
||||
|
||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||
|
||||
if (!teamLeader) { // why would this happen?
|
||||
@@ -93,12 +103,7 @@ async function updateTeamTasks (team) {
|
||||
export default async function processTeamsCron () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
}, { cron: 1, leader: 1, purchased: 1 }).exec();
|
||||
|
||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(cronPromises);
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import { Visitor } from 'universal-analytics';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
|
||||
sandbox.stub(Visitor.prototype, 'event');
|
||||
sandbox.stub(Visitor.prototype, 'transaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -37,8 +33,6 @@ describe('analyticsService', () => {
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
@@ -49,6 +43,11 @@ describe('analyticsService', () => {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -295,6 +294,9 @@ describe('analyticsService', () => {
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
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', () => {
|
||||
let data; let
|
||||
itemSpy;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
itemSpy = sandbox.stub().returnsThis();
|
||||
|
||||
Visitor.prototype.transaction.returns({
|
||||
item: itemSpy,
|
||||
send: sandbox.stub().yields(),
|
||||
});
|
||||
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
@@ -370,6 +347,11 @@ describe('analyticsService', () => {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -533,6 +515,9 @@ describe('analyticsService', () => {
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('bug-report', () => {
|
||||
emailData: {
|
||||
BROWSER_UA: userAgent,
|
||||
REPORT_MSG: userMessage,
|
||||
USER_ANALYTICS: undefined,
|
||||
USER_CLASS: 'warrior',
|
||||
USER_CONSECUTIVE_MONTHS: 0,
|
||||
USER_COSTUME: 'false',
|
||||
@@ -44,7 +45,6 @@ describe('bug-report', () => {
|
||||
USER_HOURGLASSES: 0,
|
||||
USER_ID: userId,
|
||||
USER_LEVEL: 1,
|
||||
USER_OFFSET_MONTHS: 0,
|
||||
USER_PAYMENT_PLATFORM: undefined,
|
||||
USER_SUBSCRIPTION: undefined,
|
||||
USER_TIMEZONE_OFFSET: 0,
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
||||
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 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 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 mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -195,9 +195,10 @@ describe('emails', () => {
|
||||
};
|
||||
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 mailingInfo = {
|
||||
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', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
@@ -232,7 +234,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -252,7 +254,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
||||
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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let iapIsCanceledStub;
|
||||
let iapIsExpiredStub;
|
||||
let paymentBuySkuStub;
|
||||
let iapGetPurchaseDataStub;
|
||||
let validateGiftMessageStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if no active subscription is found', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||
.returns(true);
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: 'subscription1month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let paymentBuySkuStub;
|
||||
let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.isExpired.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
const laterDate = moment.utc().add(7, 'days');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
.returns([{
|
||||
startTimeMillis: expirationDate.valueOf(),
|
||||
expirationDate,
|
||||
autoRenewing: false,
|
||||
}, {
|
||||
startTimeMillis: laterDate.valueOf(),
|
||||
expirationDate: laterDate,
|
||||
autoRenewing: false,
|
||||
},
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ autoRenewing: true }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ expirationDate, autoRenewing: false }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -715,7 +715,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
const mysteryItem = { title: 'item' };
|
||||
const mysteryItems = [mysteryItem];
|
||||
const consecutive = {
|
||||
trinkets: 3,
|
||||
trinkets: 4,
|
||||
gemCapExtra: 20,
|
||||
offset: 1,
|
||||
count: 13,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../../../../helpers/api-unit.helper';
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -65,7 +66,6 @@ describe('payments/index', () => {
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
@@ -108,14 +108,8 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
it('add a transaction entry to the recipient', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
const transactions = await TransactionModel
|
||||
.find({ userId: recipient._id })
|
||||
.sort({ createdAt: -1 })
|
||||
@@ -177,6 +171,45 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('does not reset gemCapExtra if they already had one', async () => {
|
||||
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('sets gemCapExtra to 0 if they receive a 3 month sub', async () => {
|
||||
data.gift.subscription.key = 'basic_3mo';
|
||||
data.gift.subscription.months = 3;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('sets gemCapExtra to max if they receive a 12 month sub', async () => {
|
||||
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
data.gift.subscription.key = 'basic_12mo';
|
||||
data.gift.subscription.months = 12;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('gives user 1 hourglass if they have no active subscription', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('does not give any hourglasses if they have an active subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(plan.consecutive.trinkets);
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
@@ -235,116 +268,6 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = -1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
|
||||
recipient.purchased.plan.perkMonthCount = 0;
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount goes over 3', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
||||
|
||||
@@ -421,8 +344,8 @@ describe('payments/index', () => {
|
||||
context('Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([{
|
||||
...common.content.events.winter2021Promo,
|
||||
event: 'winter2021',
|
||||
...REPEATING_EVENTS.giftOneGetOne,
|
||||
event: 'g1g1',
|
||||
}]);
|
||||
});
|
||||
|
||||
@@ -438,22 +361,30 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
||||
user.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
@@ -466,10 +397,12 @@ describe('payments/index', () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
@@ -484,11 +417,15 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends a private message about the promotion', async () => {
|
||||
@@ -511,7 +448,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||
@@ -549,33 +485,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
||||
});
|
||||
|
||||
it('keeps plan.perkMonthCount when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('updates plan.consecutive.offset when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
data.sub.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(6);
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -694,6 +603,7 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -741,55 +651,20 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Block subscription perks', () => {
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
||||
});
|
||||
|
||||
it('resets plans.consecutive.offset if 1 month subscription', async () => {
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
await user.save();
|
||||
data.sub.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
||||
});
|
||||
|
||||
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
it('adds 26 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('does not raise plan.consecutive.gemCapExtra higher than 25', async () => {
|
||||
it('does not raise plan.consecutive.gemCapExtra higher than 26', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
|
||||
@@ -798,20 +673,29 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 2 plan.consecutive.trinkets for 6 month block', async () => {
|
||||
it('adds 1 plan.consecutive.trinkets for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 4 plan.consecutive.trinkets for 12 month block', async () => {
|
||||
it('adds 1 plan.consecutive.trinkets for 12 month block if they had promo', async () => {
|
||||
user.purchased.plan.hourglassPromoReceived = new Date();
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 12 plan.consecutive.trinkets for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
@@ -819,70 +703,38 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payDifference' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -894,7 +746,7 @@ describe('payments/index', () => {
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -902,70 +754,39 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payFull' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -977,7 +798,7 @@ describe('payments/index', () => {
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -988,30 +809,13 @@ describe('payments/index', () => {
|
||||
data.updatedFrom = { logic: 'refundAndRepay' };
|
||||
});
|
||||
context('Upgrades within first half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-10'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
@@ -1019,28 +823,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-05'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1054,17 +840,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1072,35 +858,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
it('2 plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1108,10 +876,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-08-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1125,11 +893,11 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-07-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
context('Upgrades within second half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
it('Adds 0 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
@@ -1144,16 +912,16 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-20'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
@@ -1161,17 +929,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-24'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
@@ -1179,17 +947,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1197,10 +965,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1214,17 +982,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-03-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
@@ -1232,17 +1000,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1250,10 +1018,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1267,7 +1035,7 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-09-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
@@ -1277,22 +1045,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
@@ -1300,28 +1052,12 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
|
||||
@@ -1331,12 +1067,12 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1453,6 +1189,32 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset gemCapExtra', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 12;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(12);
|
||||
});
|
||||
|
||||
it('initializes gemCapExtra', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('initializes hourglasses', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset owned hourglasses', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 12;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(12);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: undefined,
|
||||
sub: undefined,
|
||||
gemsBlock: gemsBlockKey,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.not.be.called;
|
||||
@@ -101,6 +102,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
@@ -155,6 +157,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||
@@ -192,6 +195,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||
@@ -258,6 +262,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
server_url: BASE_URL,
|
||||
groupId,
|
||||
};
|
||||
|
||||
@@ -328,8 +333,9 @@ describe('Stripe - Checkout', () => {
|
||||
user.purchased.plan.customerId = customerId;
|
||||
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-user',
|
||||
userId: user._id,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user }, stripe);
|
||||
@@ -418,6 +424,7 @@ describe('Stripe - Checkout', () => {
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-group',
|
||||
server_url: BASE_URL,
|
||||
groupId,
|
||||
};
|
||||
|
||||
@@ -455,6 +462,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: anotherUser._id,
|
||||
type: 'edit-card-group',
|
||||
groupId,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);
|
||||
|
||||
@@ -308,6 +308,7 @@ describe('Stripe - One Time Payments', () => {
|
||||
customerId,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
autoRenews: false,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,6 +173,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId: null,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,6 +198,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -231,6 +233,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as subscriptions from '../../../../../../website/server/libs/payments/s
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const stripe = stripeModule('test');
|
||||
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
||||
const headers = {};
|
||||
@@ -284,7 +285,9 @@ describe('Stripe - Webhooks', () => {
|
||||
const session = {};
|
||||
|
||||
beforeEach(() => {
|
||||
session.metadata = {};
|
||||
session.metadata = {
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
event = { type: eventType, data: { object: session } };
|
||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||
constructEventStub.returns(event);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
|
||||
|
||||
const authPath = '../../../../website/server/middlewares/auth';
|
||||
|
||||
describe('auth middleware', () => {
|
||||
let res; let req; let
|
||||
@@ -16,6 +19,7 @@ describe('auth middleware', () => {
|
||||
|
||||
describe('auth with headers', () => {
|
||||
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({
|
||||
userFieldsToExclude: ['items'],
|
||||
});
|
||||
@@ -35,6 +39,7 @@ describe('auth middleware', () => {
|
||||
});
|
||||
|
||||
it('makes sure some fields are always included', done => {
|
||||
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: [
|
||||
'items', 'auth.timestamps',
|
||||
@@ -60,5 +65,57 @@ describe('auth middleware', () => {
|
||||
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 requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import { model as Blocker } from '../../../../website/server/models/blocker';
|
||||
import common from '../../../../website/common';
|
||||
|
||||
const pathToUserSchema = '../../../../website/server/models/user/schema';
|
||||
|
||||
describe('User Model', () => {
|
||||
describe('.toJSON()', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
@@ -912,4 +916,73 @@ describe('User Model', () => {
|
||||
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 {
|
||||
requester,
|
||||
translate as t,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
|
||||
@@ -56,4 +57,28 @@ describe('GET /content', () => {
|
||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||
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.getMonth()).to.eql(today.getMonth());
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('PUT /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
@@ -60,12 +61,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(heroRes.contributor.level).to.equal(1);
|
||||
expect(heroRes.purchased.ads).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(hero.contributor.level).to.equal(1);
|
||||
expect(hero.purchased.ads).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||
@@ -136,12 +137,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(heroRes.contributor.level).to.equal(6);
|
||||
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(hero.contributor.level).to.equal(6);
|
||||
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
});
|
||||
|
||||
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
@@ -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');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// @TODO Add required format
|
||||
const startDate = moment().subtract('1', 'days').toISOString();
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
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.id).to.exist;
|
||||
@@ -35,7 +35,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -238,6 +238,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
||||
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 () => {
|
||||
const user = await generateUser();
|
||||
|
||||
|
||||
@@ -110,6 +110,18 @@ describe('POST /user/auth/local/login', () => {
|
||||
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 () => {
|
||||
await user.unset({
|
||||
'auth.local.hashed_password': 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
translate as t,
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -131,6 +144,36 @@ describe('POST /user/auth/social', () => {
|
||||
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 () => {
|
||||
const response = await user.post(endpoint, {
|
||||
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');
|
||||
});
|
||||
|
||||
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,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
expect(response).to.eql({});
|
||||
|
||||
expect(response).to.exist;
|
||||
expect(response.apiToken).to.exist;
|
||||
|
||||
await user.sync();
|
||||
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 () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
password,
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('POST /user/buy-mystery-set/:key', () => {
|
||||
|
||||
expect(res.data).to.eql({
|
||||
items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
|
||||
purchasedPlanConsecutive: user.purchased.plan.consecutive,
|
||||
purchasedPlanConsecutive: JSON.parse(JSON.stringify(user.purchased.plan.consecutive)),
|
||||
});
|
||||
expect(res.message).to.equal(t('hourglassPurchaseSet'));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
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', () => {
|
||||
const now = new Date(2022, 5, 1);
|
||||
|
||||
function baseUserData (count, offset, planId) {
|
||||
return {
|
||||
purchased: {
|
||||
@@ -192,7 +190,7 @@ describe('cron utility functions', () => {
|
||||
consecutive: {
|
||||
count,
|
||||
offset,
|
||||
gemCapExtra: 25,
|
||||
gemCapExtra: 26,
|
||||
trinkets: 19,
|
||||
},
|
||||
quantity: 1,
|
||||
@@ -213,52 +211,19 @@ describe('cron utility functions', () => {
|
||||
};
|
||||
}
|
||||
|
||||
it('monthly plan, next date in 3 months', () => {
|
||||
it('elapsedMonths is 0 if its the same month', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 0;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
|
||||
const planContext = getPlanContext(user, new Date(2022, 4, 20));
|
||||
expect(planContext.elapsedMonths).to.equal(0);
|
||||
});
|
||||
|
||||
it('monthly plan, next date in 1 month', () => {
|
||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
it('elapsedMonths is 1 after one month', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
const planContext = getPlanContext(user, new Date(2022, 5, 11));
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan, no offset', () => {
|
||||
const user = baseUserData(60, 0, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with offset', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with perk count', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
expect(planContext.elapsedMonths).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -19,8 +19,8 @@ describe('releaseDates', () => {
|
||||
});
|
||||
describe('armoire', () => {
|
||||
it('should only contain valid armoire names', () => {
|
||||
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.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}-22`));
|
||||
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
|
||||
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
|
||||
});
|
||||
@@ -40,8 +40,8 @@ describe('releaseDates', () => {
|
||||
|
||||
describe('eggs', () => {
|
||||
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}`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.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}-${lastReleaseDate.day + 1}`));
|
||||
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
|
||||
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
|
||||
});
|
||||
@@ -61,8 +61,8 @@ describe('releaseDates', () => {
|
||||
|
||||
describe('hatchingPotions', () => {
|
||||
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}`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.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}-${lastReleaseDate.day + 1}`));
|
||||
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
|
||||
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// eslint-disable-next-line max-len
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import QUEST_BUNDLES from '../../website/common/script/content/bundles';
|
||||
import potions from '../../website/common/script/content/hatching-potions';
|
||||
import SPELLS from '../../website/common/script/content/spells';
|
||||
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) {
|
||||
expect(matcher.end).to.be.a('date');
|
||||
@@ -131,15 +133,21 @@ describe('Content Schedule', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
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', () => {
|
||||
const date = new Date('2024-12-22');
|
||||
const date = new Date('2025-02-28');
|
||||
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', () => {
|
||||
@@ -182,7 +190,7 @@ describe('Content Schedule', () => {
|
||||
const date = new Date('2024-04-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
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('Porcelain')).to.not.equal(-1);
|
||||
});
|
||||
@@ -222,6 +230,8 @@ describe('Content Schedule', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||
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;
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
@@ -281,19 +306,26 @@ describe('Content Schedule', () => {
|
||||
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 matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey022024')).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 matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey022023')).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', () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Shop Featured Items', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
||||
});
|
||||
|
||||
@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
|
||||
sinonStubPromise(global.sinon);
|
||||
global.sandbox = sinon.createSandbox();
|
||||
|
||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||
|
||||
setupNconf('./config.json.example');
|
||||
|
||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
||||
}
|
||||
|
||||
before(done => {
|
||||
mongoose.connection.on('open', err => {
|
||||
if (err) return done(err);
|
||||
return resetHabiticaDB()
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
mongoose.connection.once('open', async err => {
|
||||
if (err) throw err;
|
||||
await resetHabiticaDB();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const nconf = require('nconf');
|
||||
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
|
||||
require('@babel/register');
|
||||
|
||||
@@ -3,11 +3,12 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
|
||||
rules: {
|
||||
'no-console': 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'],
|
||||
}],
|
||||
},
|
||||
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',
|
||||
],
|
||||
};
|
||||
@@ -12,6 +12,7 @@
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
||||
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-screen">
|
||||
@@ -28,10 +29,9 @@
|
||||
</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>
|
||||
<!-- 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>
|
||||
</html>
|
||||
12170
website/client/package-lock.json
generated
@@ -3,62 +3,63 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
||||
"lint": "vue-cli-service lint .",
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
||||
"serve": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-unit-mocha": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.3",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^0.28.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"core-js": "^3.33.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"ga-gtag": "^1.2.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-babel-compiler": "^2.0.0",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@vitest/browser": "^3.0.5",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"chai": "^5.1.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
</div>
|
||||
<snackbars />
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<user-main v-else />
|
||||
<div v-else>
|
||||
<user-main />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#loading-screen-inapp {
|
||||
#melior {
|
||||
@@ -90,7 +92,7 @@
|
||||
</style>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: .9 !important;
|
||||
@@ -106,18 +108,17 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import userMain from '@/pages/user-main';
|
||||
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 {
|
||||
name: 'App',
|
||||
components: {
|
||||
snackbars,
|
||||
userMain,
|
||||
userMain: () => import('@/pages/user-main'),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -148,10 +149,6 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
Analytics.load();
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
@@ -210,22 +207,33 @@ export default {
|
||||
const isBanned = this.checkForBannedUser(error);
|
||||
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.
|
||||
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) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// Chat paging should help this, but maybe we can also find another solution..
|
||||
return Promise.resolve(error);
|
||||
if (configExists) {
|
||||
if (error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// 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 errorMessage = errorData.message || errorData;
|
||||
const errorCode = errorData.error;
|
||||
|
||||
// Check for conditions to reset the user auth
|
||||
// TODO use a specific error like NotificationNotFound instead of checking for the string
|
||||
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
|
||||
if (invalidUserMessage.indexOf(errorMessage) !== -1) {
|
||||
// If 'invalid_credentials' signaled, force logout
|
||||
if (error.response.status === 401 && errorCode === 'invalid_credentials') {
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return null;
|
||||
}
|
||||
@@ -268,16 +276,29 @@ export default {
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
|
||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||
this.hideLoadingScreen();
|
||||
// Check if we need to show password change success message
|
||||
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: {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
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 errorMessage = error.response.data.message;
|
||||
|
||||
@@ -301,4 +322,3 @@ export default {
|
||||
</script>
|
||||
|
||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
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;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -47,6 +48,10 @@
|
||||
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 {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -172,7 +177,7 @@
|
||||
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;
|
||||
height: 135px;
|
||||
}
|
||||
@@ -185,6 +190,14 @@
|
||||
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 {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
|
||||
BIN
website/client/src/assets/images/confetti.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
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;
|
||||
}
|
||||
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||
|
||||
@each $foolPet in $foolPets {
|
||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.featured-label {
|
||||
width: auto;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
$grid-gutter-width: 24px;
|
||||
|
||||
// Bootstrap and its default variables
|
||||
@import 'node_modules/bootstrap/scss/bootstrap';
|
||||
@import '~/bootstrap/scss/bootstrap';
|
||||
|
||||
// Bootstrap Vue styles
|
||||
@import 'node_modules/bootstrap-vue/dist/bootstrap-vue';
|
||||
@import '~/bootstrap-vue/dist/bootstrap-vue';
|
||||
@@ -3,14 +3,14 @@
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
border: 1px solid transparent;
|
||||
padding: 4px 12px;
|
||||
line-height: 1.714;
|
||||
border: 2px solid transparent;
|
||||
padding: 2px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
color: $white;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover:not(:disabled):not(.disabled), &:focus {
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
|
||||
&.btn-flat {
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:active, &.active:not(.btn-flat) {
|
||||
@@ -28,11 +28,11 @@
|
||||
|
||||
&:disabled, &.disabled {
|
||||
cursor: default;
|
||||
color: $gray-50;
|
||||
opacity: 0.75;
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
background-color: $gray-700;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
.svg {
|
||||
color: $gray-300;
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
height: 2rem; // otherwise would something set the height to 33px
|
||||
height: 32px; // otherwise would something set the height to 33px
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -48,40 +48,47 @@
|
||||
}
|
||||
|
||||
.btn-front {
|
||||
border: none !important;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
padding: 7.5px 15.5px;
|
||||
padding: 2px 17px;
|
||||
|
||||
&:hover {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: $purple-200;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
line-height: 1.714;
|
||||
--icon-color: #{$purple-500};
|
||||
|
||||
&:focus {
|
||||
background: $purple-200;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:hover {
|
||||
background: #5d3b9c;
|
||||
border: 1px solid transparent;
|
||||
background: $purple-200;
|
||||
border: 2px solid transparent;
|
||||
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:active, &.active {
|
||||
background: $purple-200;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: none;
|
||||
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:active:focus, &.active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,42 +101,45 @@
|
||||
|
||||
.btn-secondary,
|
||||
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success)
|
||||
{
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success) {
|
||||
background: $white;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
color: $gray-50;
|
||||
|
||||
--icon-color: #{$gray-200};
|
||||
|
||||
&:focus, &:active {
|
||||
color: $gray-50;
|
||||
background: $white;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
color: $gray-50;
|
||||
|
||||
--icon-color: #{$purple-300};
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:active, &.active {
|
||||
background: $white;
|
||||
border: 2px solid $purple-400;
|
||||
color: $purple-300;
|
||||
--icon-color: #{$purple-300};
|
||||
|
||||
&:focus {
|
||||
color: $purple-300;
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
background: $white;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $purple-300;
|
||||
|
||||
background: $white !important;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
--icon-color: #{$purple-300};
|
||||
.svg {
|
||||
@@ -151,91 +161,118 @@
|
||||
|
||||
.btn-danger {
|
||||
background: $maroon-100;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: #e14e4e;
|
||||
border: 1px solid transparent;
|
||||
background: $maroon-100;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $maroon-100;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
background: $maroon-100;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: $orange-10;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
color: $white !important;
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: $orange-100;
|
||||
background: $orange-10;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $orange-10;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
background: $orange-10;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: $green-50;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: #32bd8a;
|
||||
border: 1px solid transparent;
|
||||
background: $green-50;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $green-50;
|
||||
border-color: $purple-400;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
background: $green-50;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: $blue-50;
|
||||
background-color: $blue-100;
|
||||
color: $black;
|
||||
font-weight: 700;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
&:disabled {
|
||||
background: $blue-50;
|
||||
background-color: $white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: $blue-100;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $blue-100;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: $blue-50;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(.disabled), &.active:not(:disabled):not(.disabled) {
|
||||
background: $blue-50;
|
||||
background: $blue-100;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +281,7 @@
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
line-height: 1.714;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background: $gray-500;
|
||||
@@ -262,12 +299,28 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-flat,
|
||||
.dropdown > .btn-flat.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-flat.dropdown-toggle:not(.btn-success) {
|
||||
&.with-icon {
|
||||
.svg-icon.color {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
padding: 4px 8px;
|
||||
line-height: 2;
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
@@ -78,15 +78,3 @@ $gold-color: #FFA624;
|
||||
$hourglass-color: #2995CD;
|
||||
|
||||
$purple-task: #925cf3;
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
.purple-300 {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
.dropdown > .btn {
|
||||
padding: 0.219rem 0.75rem;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 1.714;
|
||||
padding: 2px 12px;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
@@ -33,11 +34,16 @@
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
border: transparent;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-min-width {
|
||||
.dropdown-menu {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// shared dropdown-item styles
|
||||
@@ -53,6 +59,8 @@
|
||||
color: $gray-50 !important;
|
||||
cursor: pointer;
|
||||
|
||||
--dropdown-item-hover-icon-color: #{$gray-200};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: inherit;
|
||||
@@ -87,7 +95,7 @@
|
||||
|
||||
&:not(:hover) {
|
||||
.with-icon .svg-icon {
|
||||
color: $gray-200;
|
||||
color: var(dropdown-item-hover-icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +121,10 @@
|
||||
}
|
||||
|
||||
.dropdown-icon-item {
|
||||
line-height: 1;
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
|
||||
.svg-icon {
|
||||
margin: 0px 16px 0px 0px;
|
||||
vertical-align: middle;
|
||||
@@ -128,7 +140,6 @@
|
||||
|
||||
.dropdown-toggle {
|
||||
width: 100% !important;
|
||||
height: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -147,7 +158,7 @@
|
||||
|
||||
// selectList.vue items sizing
|
||||
.selectListItem .dropdown-item {
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: 0.25rem 1rem 0.25rem 0.75rem;
|
||||
height: 32px;
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h1 {
|
||||
margin-top: 0px;
|
||||
@@ -44,6 +44,10 @@ ul {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
font-size: 1em;
|
||||
color: $gray-10;
|
||||
|
||||
@@ -61,13 +61,13 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 27px;
|
||||
background-image: url(~@/assets/svg/for-css/check.svg);
|
||||
background-image: url(@/assets/svg/for-css/check.svg);
|
||||
background-size: 1rem;
|
||||
}
|
||||
|
||||
&.input-invalid {
|
||||
padding-right: 40px;
|
||||
background-image: url(~@/assets/svg/for-css/alert.svg);
|
||||
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||
background-size: 16px 16px;
|
||||
border-color: $red-100 !important;
|
||||
}
|
||||
@@ -239,7 +239,7 @@ $bg-disabled-control: $gray-10;
|
||||
&:checked~.custom-control-label::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
}
|
||||
|
||||
|
||||
168
website/client/src/assets/scss/forms.scss
Normal file
@@ -0,0 +1,168 @@
|
||||
// Inputs and textareas
|
||||
|
||||
input, textarea, input.form-control, textarea.form-control {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
padding: 4px 12px;
|
||||
color: $gray-50;
|
||||
border: 1px solid $gray-400;
|
||||
|
||||
&:hover:not(:disabled):not(:read-only):not(:focus):not(:disabled):not(.input-valid):not(.input-invalid):not(.dark) {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only),
|
||||
&:active:not(:disabled):not(:read-only).dark, &:focus:not(:disabled):not(:read-only).dark {
|
||||
border: 1px solid $purple-400;
|
||||
outline: 1px solid $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 27px;
|
||||
background-image: url(@/assets/svg/for-css/check.svg);
|
||||
background-size: 1rem;
|
||||
border-color: $green-10;
|
||||
}
|
||||
|
||||
&.input-invalid, .input-invalid:hover {
|
||||
padding-right: 40px;
|
||||
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||
background-size: 16px 16px;
|
||||
border-color: $red-100;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.input-invalid.input-with-error {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
|
||||
&:not(.input-valid):not(.input-invalid) {
|
||||
border: 1px solid $purple-300;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover:not(:focus):not(:disabled):not(.input-valid):not(.input-invalid) {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $purple-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $maroon-500;
|
||||
}
|
||||
|
||||
// checkboxes
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
&:hover~.custom-control-label::before {
|
||||
border-color: $gray-100;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-300;
|
||||
border-color: $purple-300;
|
||||
}
|
||||
|
||||
&:hover:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:disabled)~.custom-control-label::before {
|
||||
background-color: $purple-400;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
}
|
||||
|
||||
&:checked:disabled~.custom-control-label::after {
|
||||
background-image: url(@/assets/svg/for-css/checkbox-gray.svg);
|
||||
}
|
||||
|
||||
&:active~.custom-control-label::before {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
~.custom-control-label::before {
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:hover~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-50;
|
||||
}
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-100;
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
|
||||
.iconalert-success::before {
|
||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
background-color: #1ca372;
|
||||
}
|
||||
|
||||
.iconalert-warning::before, .iconalert-error::before {
|
||||
background-image: url(~@/assets/svg/for-css/alert-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/alert-white.svg);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal {
|
||||
z-index: 1350;
|
||||
|
||||
16
website/client/src/assets/scss/privacy.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.privacy-banner {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: $white;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
|
||||
width: calc(66vw + 96px);
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
margin: auto 12.5%;
|
||||
}
|
||||
@media only screen and (min-width: 992px) {
|
||||
margin: auto 14.5%;
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,11 @@
|
||||
|
||||
.background {
|
||||
background-repeat: repeat-x;
|
||||
|
||||
height:216px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -67,6 +65,13 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shop-message {
|
||||
position: relative;
|
||||
height: 76px;
|
||||
margin: 71px auto;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.npc {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.container-fluid.static-view {
|
||||
margin: 5em 2em 0 2em;
|
||||
@@ -14,7 +14,7 @@
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
li, p {
|
||||
li, p:not(.purple-600) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,3 +86,103 @@ h4 {
|
||||
.opacity-75 {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: $gray-100 !important;
|
||||
}
|
||||
|
||||
.bg-gray-300 {
|
||||
background-color: $gray-300 !important;
|
||||
}
|
||||
|
||||
.bg-gray-600 {
|
||||
background-color: $gray-600 !important;
|
||||
}
|
||||
|
||||
.bg-gray-700 {
|
||||
background-color: $gray-700 !important;
|
||||
}
|
||||
|
||||
.bg-green-10 {
|
||||
background-color: $green-10 !important;
|
||||
}
|
||||
|
||||
.bg-green-100 {
|
||||
background-color: $green-100 !important;
|
||||
}
|
||||
|
||||
.bg-purple-50 {
|
||||
background-color: $purple-50 !important;
|
||||
}
|
||||
|
||||
.bg-purple-100 {
|
||||
background-color: $purple-100 !important;
|
||||
}
|
||||
|
||||
.bg-purple-300 {
|
||||
background-color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
background-color: $yellow-50 !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: $white !important;
|
||||
}
|
||||
|
||||
.gray-10 {
|
||||
color: $gray-10 !important;
|
||||
}
|
||||
|
||||
.gray-50 {
|
||||
color: $gray-50 !important;
|
||||
}
|
||||
|
||||
.gray-100 {
|
||||
color: $gray-100 !important;
|
||||
}
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
.gray-300 {
|
||||
color: $gray-300 !important;
|
||||
}
|
||||
|
||||
.green-10 {
|
||||
color: $green-10 !important;
|
||||
}
|
||||
|
||||
.maroon-50 {
|
||||
color: $maroon-50 !important;
|
||||
}
|
||||
|
||||
.purple-200 {
|
||||
color: $purple-200 !important;
|
||||
}
|
||||
|
||||
.purple-300 {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.purple-600 {
|
||||
color: $purple-600 !important;
|
||||
}
|
||||
|
||||
.teal-1 {
|
||||
color: $teal-1 !important;
|
||||
}
|
||||
|
||||
.teal-10 {
|
||||
color: $teal-10 !important;
|
||||
}
|
||||
|
||||
.yellow-10 {
|
||||
color: $yellow-10 !important;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
3
website/client/src/assets/svg/bluesky.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4,0C1.79,0,0,1.79,0,4v16c0,2.21,1.79,4,4,4h16c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4H4ZM12,11.57c-.72-1.49-2.7-4.26-4.53-5.63-1.32-.99-3.47-1.75-3.47.68,0,.49.28,4.08.44,4.66.57,2.03,2.65,2.55,4.5,2.23-3.24.55-4.06,2.36-2.28,4.17,3.38,3.44,4.85-.86,5.23-1.97h0s0,0,0,0c.07-.2.1-.29.1-.21,0-.08.03.01.1.22h0c.38,1.1,1.85,5.41,5.23,1.97,1.78-1.81.95-3.63-2.28-4.17,1.85.31,3.93-.2,4.5-2.23.16-.58.44-4.18.44-4.66,0-2.43-2.14-1.67-3.47-.68-1.83,1.37-3.81,4.14-4.53,5.63Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
32
website/client/src/assets/svg/divider-stars.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="40" height="16" viewBox="0 0 40 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.3333 5.33333L12 8L17.3333 10.6667L20 16L22.6667 10.6667L28 8L22.6667 5.33333L20 0L17.3333 5.33333Z" fill="#BDA8FF"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M25 7.99984L21.6667 9.6665L20 7.99984L25 7.99984Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20 3L21.6666 6.33333L20 8V3Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M15 8L18.3333 6.33333L20 8L15 8Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20 13L18.3333 9.66667L20 8V13Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M15 7.99984L18.3333 9.6665L20 7.99984L15 7.99984Z" fill="#925CF3"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20 3L18.3333 6.33333L20 8V3Z" fill="#925CF3"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M25 8L21.6667 6.33333L20 8L25 8Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20 13L21.6666 9.66667L20 8V13Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.7999 7.20041L22.3333 8.00041L20.7999 8.80041L19.9999 10.3337L19.1999 8.80041L17.6666 8.00041L19.1999 7.20041L19.9999 5.66707L20.7999 7.20041Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66667 6.66667L0 8L2.66667 9.33333L4 12L5.33333 9.33333L8 8L5.33333 6.66667L4 4L2.66667 6.66667Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M6.49976 7.99967L4.83309 8.83301L3.99976 7.99967H6.49976Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 5.5L4.83331 7.16667L3.99998 8L3.99998 5.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M1.5 8L3.16667 7.16667L4 8H1.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M4 10.5L3.16667 8.83333L4 8V10.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M1.50018 7.99967L3.16685 8.83301L4.00018 7.99967H1.50018Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M4 5.5L3.16667 7.16667L4 8V5.5Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.5 8L4.83333 7.16667L4 8H6.5Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 10.5L4.83331 8.83333L3.99998 8L3.99998 10.5Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M4.40002 7.59935L5.16669 7.99935L4.40002 8.39935L4.00002 9.16602L3.60002 8.39935L2.83335 7.99935L3.60002 7.59935L4.00002 6.83268L4.40002 7.59935Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.6667 6.66667L32 8L34.6667 9.33333L36 12L37.3333 9.33333L40 8L37.3333 6.66667L36 4L34.6667 6.66667Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M38.4998 7.99967L36.8331 8.83301L35.9998 7.99967H38.4998Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M36 5.5L36.8333 7.16667L36 8V5.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.5 8L35.1667 7.16667L36 8H33.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M36 10.5L35.1667 8.83333L36 8V10.5Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.5002 7.99967L35.1668 8.83301L36.0002 7.99967H33.5002Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M36 5.5L35.1667 7.16667L36 8V5.5Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M38.5 8L36.8333 7.16667L36 8H38.5Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M36 10.5L36.8333 8.83333L36 8V10.5Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M36.4 7.59935L37.1667 7.99935L36.4 8.39935L36 9.16602L35.6 8.39935L34.8334 7.99935L35.6 7.59935L36 6.83268L36.4 7.59935Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
3
website/client/src/assets/svg/for-css/checkbox-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 10">
|
||||
<path fill="#878190" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.83-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
@@ -1,7 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 31">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#FFF" d="M120.876 24.007a2.27 2.27 0 0 0-3.183.41 4.595 4.595 0 0 1-3.663 1.804 4.62 4.62 0 0 1-4.613-4.335c-.005-.35-.009-2.864-.009-3.19a4.627 4.627 0 0 1 4.622-4.622c1.28 0 2.47.51 3.353 1.44a2.269 2.269 0 0 0 3.29-3.125 9.2 9.2 0 0 0-6.643-2.853c-5.05 0-9.16 4.109-9.16 9.16 0 .03.002 3.175.014 3.406a9.158 9.158 0 0 0 9.146 8.657 9.1 9.1 0 0 0 7.257-3.57 2.27 2.27 0 0 0-.411-3.182M134.373 26.221a4.62 4.62 0 0 1-4.613-4.333c-.005-.353-.008-2.877-.008-3.193a4.627 4.627 0 0 1 4.621-4.622 4.627 4.627 0 0 1 4.622 4.622c0 .328-.003 2.84-.009 3.189a4.618 4.618 0 0 1-4.613 4.337m6.891-17.078a2.264 2.264 0 0 0-2.19 1.706 9.095 9.095 0 0 0-4.7-1.313c-5.051 0-9.16 4.109-9.16 9.16 0 .031.001 3.173.013 3.406a9.158 9.158 0 0 0 9.146 8.657 9.118 9.118 0 0 0 4.81-1.37 2.268 2.268 0 0 0 4.35-.899V11.412a2.27 2.27 0 0 0-2.269-2.269M30.546 26.221a4.62 4.62 0 0 1-4.613-4.335c-.006-.35-.01-2.863-.01-3.19a4.627 4.627 0 0 1 4.623-4.623 4.627 4.627 0 0 1 4.622 4.622c0 .328-.004 2.84-.01 3.189a4.618 4.618 0 0 1-4.612 4.337m6.89-17.078a2.264 2.264 0 0 0-2.19 1.706 9.095 9.095 0 0 0-4.7-1.313c-5.052 0-9.16 4.109-9.16 9.16 0 .031 0 3.174.013 3.406a9.158 9.158 0 0 0 9.147 8.657 9.118 9.118 0 0 0 4.809-1.37 2.268 2.268 0 0 0 4.35-.899V11.412a2.27 2.27 0 0 0-2.269-2.269M70.84 9.143a2.27 2.27 0 0 0-2.27 2.27V28.49a2.27 2.27 0 0 0 4.539 0V11.412a2.27 2.27 0 0 0-2.27-2.269M97.563 9.143a2.27 2.27 0 0 0-2.27 2.27V28.49a2.27 2.27 0 0 0 4.538 0V11.412a2.27 2.27 0 0 0-2.268-2.269M59.066 21.888a4.62 4.62 0 0 1-4.613 4.333 4.62 4.62 0 0 1-4.613-4.338c-.006-.35-.009-2.86-.009-3.187a4.627 4.627 0 0 1 4.622-4.622 4.627 4.627 0 0 1 4.622 4.622c0 .315-.004 2.84-.009 3.192M54.453 9.536a9.089 9.089 0 0 0-4.622 1.265V2.33a2.27 2.27 0 0 0-4.537 0V28.49a2.269 2.269 0 0 0 4.35.9 9.117 9.117 0 0 0 4.81 1.37 9.16 9.16 0 0 0 9.146-8.666c.011-.224.013-3.367.013-3.398 0-5.052-4.11-9.16-9.16-9.16M8.92 9.536a9.143 9.143 0 0 0-4.382 1.11V2.33A2.27 2.27 0 0 0 0 2.33v26.16a2.269 2.269 0 1 0 4.538 0V16.763c.173-.147.333-.314.46-.516a4.601 4.601 0 0 1 3.921-2.173 4.627 4.627 0 0 1 4.622 4.622c0 .415-.004 9.233-.01 9.738a2.27 2.27 0 0 0 4.535.172c.01-.225.012-9.814.012-9.91 0-5.052-4.108-9.16-9.159-9.16M88.95 9.143h-2.648V2.33a2.27 2.27 0 0 0-4.538 0v6.813h-2.647a2.27 2.27 0 0 0 0 4.538h2.647V28.49a2.27 2.27 0 0 0 4.538 0V13.681h2.647a2.27 2.27 0 0 0 0-4.538"/>
|
||||
<path fill="#FF6066" d="M73.025 2.33a2.27 2.27 0 1 1-4.538 0 2.27 2.27 0 0 1 4.538 0"/>
|
||||
<path fill="#4FB5E8" d="M99.748 2.33a2.27 2.27 0 1 1-4.539 0 2.27 2.27 0 0 1 4.539 0"/>
|
||||
</g>
|
||||
<svg width="145" height="32" viewBox="0 0 145 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M72.7484 0.0131836C71.1152 0.0131836 69.7861 1.33328 69.7861 2.96139C69.7861 4.5895 71.1152 5.90959 72.7484 5.90959C74.3816 5.90959 75.7106 4.58461 75.7106 2.96139C75.7106 1.33816 74.3816 0.0131836 72.7484 0.0131836Z" fill="#FF6165"/>
|
||||
<path d="M99.3498 0.0131836C97.7166 0.0131836 96.3875 1.33328 96.3875 2.96139C96.3875 4.5895 97.7166 5.90959 99.3498 5.90959C100.983 5.90959 102.312 4.58461 102.312 2.96139C102.312 1.33816 100.983 0.0131836 99.3498 0.0131836Z" fill="#50B5E9"/>
|
||||
<path d="M123.181 28.1326C121.263 30.5919 118.37 32 115.236 32C109.91 32 105.511 27.8784 105.221 22.6176C105.207 22.3536 105.207 19.2244 105.207 19.1951C105.207 13.7192 109.704 9.26509 115.236 9.26509C117.973 9.26509 120.626 10.3945 122.509 12.36C123.083 12.9565 123.392 13.7436 123.367 14.5699C123.348 15.3962 122.999 16.1638 122.391 16.7309C121.146 17.9044 119.169 17.8506 117.982 16.6185C117.256 15.8655 116.281 15.45 115.231 15.45C113.142 15.45 111.445 17.127 111.445 19.1902C111.445 19.5129 111.445 21.9966 111.455 22.3389C111.572 24.2701 113.235 25.8102 115.231 25.8102C116.418 25.8102 117.511 25.2773 118.232 24.3484C119.282 22.9989 121.249 22.7496 122.607 23.791C123.269 24.2946 123.691 25.0231 123.799 25.8445C123.907 26.6659 123.681 27.4775 123.176 28.1326H123.181ZM144.995 12.316V28.9442C144.995 30.6261 143.627 31.9951 141.945 31.9951C141.047 31.9951 140.213 31.5991 139.645 30.9439C138.286 31.6333 136.785 31.9951 135.26 31.9951C130.091 31.9951 125.819 27.9419 125.54 22.7642C125.525 22.5051 125.525 19.4347 125.525 19.4005C125.525 14.0174 129.895 9.64156 135.26 9.64156C136.702 9.64156 138.124 9.96914 139.434 10.595C139.988 9.77846 140.92 9.2602 141.95 9.2602C143.632 9.2602 145 10.6292 145 12.3111L144.995 12.316ZM138.889 22.4807C138.894 22.0798 138.899 19.7183 138.899 19.4053C138.899 17.3861 137.266 15.7482 135.26 15.7482C133.254 15.7482 131.621 17.391 131.621 19.4053C131.621 19.7134 131.621 22.1629 131.631 22.5051C131.744 24.3875 133.343 25.8934 135.26 25.8934C137.178 25.8934 138.767 24.3972 138.889 22.4807ZM42.3338 12.316V28.9442C42.3338 30.6261 40.9655 31.9951 39.2833 31.9951C38.3858 31.9951 37.5521 31.5991 36.9832 30.9439C35.6247 31.6333 34.124 31.9951 32.5988 31.9951C27.4247 31.9951 23.158 27.9419 22.8785 22.7642C22.8638 22.5345 22.8638 20.1338 22.8638 19.4005C22.8638 14.0174 27.2335 9.64156 32.5988 9.64156C34.0406 9.64156 35.4678 9.96914 36.7723 10.595C37.3265 9.77846 38.2583 9.2602 39.2882 9.2602C40.9704 9.2602 42.3387 10.6292 42.3387 12.3111L42.3338 12.316ZM32.5988 15.7482C30.5929 15.7482 28.9598 17.391 28.9598 19.4053C28.9598 19.7231 28.9598 22.158 28.9696 22.5002C29.0824 24.3875 30.6812 25.8934 32.5988 25.8934C34.5163 25.8934 36.1053 24.3972 36.2279 22.4807C36.2328 22.0798 36.2377 19.7231 36.2377 19.4053C36.2377 17.3861 34.6046 15.7482 32.5988 15.7482ZM72.7452 9.42643C71.1121 9.42643 69.783 10.7954 69.783 12.4773V28.9442C69.783 30.6261 71.1121 31.9951 72.7452 31.9951C74.3783 31.9951 75.7074 30.6261 75.7074 28.9442V12.4773C75.7074 10.7954 74.3783 9.42643 72.7452 9.42643ZM99.346 9.42643C97.7129 9.42643 96.3838 10.7954 96.3838 12.4773V28.9442C96.3838 30.6261 97.7129 31.9951 99.346 31.9951C100.979 31.9951 102.308 30.6261 102.308 28.9442V12.4773C102.308 10.7954 100.979 9.42643 99.346 9.42643ZM66.041 19.244C66.041 19.2733 66.0411 22.3878 66.0264 22.6469C65.7468 27.893 61.4752 32 56.3061 32C54.7808 32 53.2801 31.6333 51.9216 30.9342C51.3527 31.5991 50.519 32 49.6166 32C47.9344 32 46.5662 30.6164 46.5662 28.9198V3.08021C46.5662 1.38365 47.9344 0 49.6166 0C51.2988 0 52.6671 1.38365 52.6671 3.08021V10.0816C53.8245 9.60734 55.0604 9.35798 56.3061 9.35798C61.6762 9.35798 66.041 13.7925 66.041 19.244ZM56.3061 15.5233C54.3002 15.5233 52.6671 17.1905 52.6671 19.244C52.6671 19.5667 52.6671 22.0358 52.6769 22.3829C52.7946 24.3044 54.3885 25.8347 56.3061 25.8347C58.2236 25.8347 59.8077 24.3141 59.9352 22.3682C59.9352 21.9526 59.945 19.552 59.945 19.244C59.945 17.1905 58.3119 15.5233 56.3061 15.5233ZM9.62221 9.35798C8.43537 9.35798 7.26325 9.57311 6.15978 9.98381V3.0851C6.15978 1.38854 4.77677 0.00488934 3.07989 0.00488934C1.38301 0.00488934 0 1.38854 0 3.0851V28.9198C0 30.6212 1.38301 32 3.07989 32C4.77677 32 6.15978 30.6164 6.15978 28.9198V17.6941C6.28729 17.5572 7.4349 15.5233 9.62221 15.5233C11.6624 15.5233 13.32 17.1905 13.32 19.244C13.32 19.6547 13.32 28.3526 13.3102 28.8416C13.2661 30.5235 14.551 31.912 16.2185 31.9951C16.2773 31.9951 16.3313 31.9951 16.3901 31.9951C18.0281 31.9951 19.3768 30.7092 19.4602 29.0714C19.4749 28.8171 19.4749 19.3369 19.4749 19.244C19.4749 13.7925 15.0561 9.35798 9.62221 9.35798ZM91.2147 9.35798H89.41V3.08021C89.41 1.38365 88.0122 0 86.2908 0C84.5694 0 83.1717 1.38365 83.1717 3.08021V9.35798H81.3669C79.6504 9.35798 78.2478 10.7416 78.2478 12.4431C78.2478 14.1445 79.6455 15.5233 81.3669 15.5233H83.1717V28.9198C83.1717 30.6212 84.5694 32 86.2908 32C88.0122 32 89.41 30.6164 89.41 28.9198V15.5233H91.2147C92.9361 15.5233 94.3339 14.1396 94.3339 12.4431C94.3339 10.7465 92.9361 9.35798 91.2147 9.35798Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.0 KiB |
48
website/client/src/assets/svg/hourglass-sparkle-left.svg
Normal file
@@ -0,0 +1,48 @@
|
||||
<svg width="53" height="53" viewBox="0 0 53 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.4728 37.2881L31.0401 40.6634C30.8722 41.0588 30.533 41.3618 30.1106 41.4581C25.4964 42.5161 15.7591 38.3829 13.3131 34.3281C13.0907 33.958 13.073 33.5035 13.2408 33.108L14.6735 29.7328C15.9802 26.6545 18.829 24.3765 21.9907 23.6412C22.9143 23.4268 23.3484 22.4041 22.8611 21.5907C21.1937 18.8056 20.8534 15.174 22.16 12.0957L23.5927 8.72043C23.7606 8.32501 24.0998 8.02203 24.5199 7.92663C29.1358 6.86935 38.8731 11.0026 41.3173 15.0567C41.5422 15.4259 41.5599 15.8804 41.392 16.2758L39.9593 19.651C38.6527 22.7293 35.8039 25.0073 32.6422 25.7426C31.7185 25.957 31.2844 26.9797 31.7718 27.7931C33.4392 30.5782 33.7795 34.2099 32.4728 37.2881Z" fill="#A9DCF6" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.664 12.0938C29.7729 10.8666 27.3007 10.4316 25.8346 10.5102L24.7036 13.1747C23.7617 15.3938 23.9622 18.0859 25.2286 20.2007C25.9064 21.3331 26.0063 22.7223 25.4978 23.9203C24.9893 25.1182 23.9206 26.0114 22.6375 26.3096C20.2342 26.8686 18.1583 28.5945 17.2171 30.8118L16.0854 33.478C17.0473 34.5873 19.0775 36.0634 21.9686 37.2906C24.8596 38.5178 27.3318 38.9528 28.7979 38.8742L29.9297 36.208C30.8709 33.9906 30.6703 31.2985 29.4039 29.1837C28.7261 28.0513 28.6263 26.662 29.1348 25.4641C29.6433 24.2662 30.7119 23.3729 31.9957 23.0732C34.3983 22.5158 36.4742 20.7899 37.4162 18.5709L38.5472 15.9064C37.5853 14.7971 35.555 13.321 32.664 12.0938Z" fill="white" fill-opacity="0.9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.5684 13.8635C33.7371 14.784 35.9512 15.9582 35.7471 16.4997C35.5853 16.9371 35.2218 17.7653 33.0852 17.902C31.0091 18.0368 30.0196 19.4736 29.0902 19.0791C28.0092 18.6202 28.3314 17.6878 27.3948 16.2289C26.429 14.7261 26.743 13.3826 26.9685 12.8935C27.2828 12.2137 29.3996 12.9429 31.5684 13.8635Z" fill="#9A62FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.3942 16.2291C26.4285 14.7263 26.7424 13.3828 26.9679 12.8937C27.2164 12.3553 28.5959 12.7006 30.2369 13.3243C29.6862 13.3623 28.8925 13.5984 28.6315 14.6159C28.3585 15.6755 29.4738 17.4486 28.8447 18.2192C28.7229 18.3703 28.5806 18.4714 28.4271 18.5381C28.0907 18.0153 28.0509 17.2522 27.3942 16.2291Z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.3429 26.9852C27.003 27.2653 26.6667 28.5305 27.3103 29.6779C28.1003 31.0844 28.7422 32.6761 28.3659 34.4146C28.0561 35.8466 27.757 35.9145 27.4109 35.8779C27.0665 35.842 24.5481 35.2396 22.4522 33.773C20.3546 32.3057 19.4609 30.8532 20.0783 30.0166C20.6666 29.2207 21.6339 28.7393 23.1921 28.369C24.6835 28.0155 25.6829 26.705 26.3429 26.9852Z" fill="#9A62FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.4114 35.8785C27.0653 35.8419 24.5469 35.2396 22.451 33.773C20.7841 32.6066 19.878 31.4501 19.8876 30.6036C19.8956 30.5893 19.9037 30.575 19.9144 30.5637C20.2509 30.1316 22.3027 30.658 24.5368 32.0612C26.6666 33.3964 28.1998 34.9294 27.8338 35.7684C27.6986 35.8902 27.5598 35.8942 27.4114 35.8785Z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.80368 18.2264L8.2016e-05 20.1109L3.80368 21.9953L5.70549 25.7643L7.60729 21.9953L11.4109 20.1109L7.60729 18.2264L5.70549 14.4574L3.80368 18.2264Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M9.27148 20.1109L6.89423 21.2887L5.70561 20.1109H9.27148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M5.70542 16.5775L6.89404 18.933L5.70542 20.1108V16.5775Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M2.13969 20.1108L4.51694 18.933L5.70557 20.1108H2.13969Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M5.70557 23.6443L4.51694 21.2887L5.70557 20.1109V23.6443Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M2.13969 20.1109L4.51694 21.2887L5.70557 20.1109H2.13969Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M5.70557 16.5775L4.51694 18.933L5.70557 20.1108V16.5775Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M9.27148 20.1108L6.89423 18.933L5.70561 20.1108H9.27148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M5.70542 23.6443L6.89404 21.2887L5.70542 20.1109V23.6443Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.27609 19.5455L7.36963 20.1109L6.27609 20.6762L5.70555 21.7598L5.13501 20.6762L4.04148 20.1109L5.13501 19.5455L5.70555 18.4619L6.27609 19.5455Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8625 1.53112L11.3172 2.29668L12.8625 3.06225L13.6351 4.59338L14.4077 3.06225L15.9529 2.29668L14.4077 1.53112L13.6351 -1.81198e-05L12.8625 1.53112Z" fill="#BDA8FF"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M15.0837 2.29667L14.118 2.77515L13.6351 2.29667H15.0837Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M13.635 0.861193L14.1179 1.81815L13.635 2.29663V0.861193Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M12.1864 2.29663L13.1521 1.81815L13.635 2.29663H12.1864Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M13.635 3.73218L13.1521 2.77522L13.635 2.29674V3.73218Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M12.1864 2.29667L13.1521 2.77515L13.635 2.29667H12.1864Z" fill="#925CF3"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M13.635 0.861193L13.1521 1.81815L13.635 2.29663V0.861193Z" fill="#925CF3"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M15.0837 2.29663L14.118 1.81815L13.6351 2.29663H15.0837Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M13.635 3.73218L14.1179 2.77522L13.635 2.29674V3.73218Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M13.8668 2.06701L14.311 2.29668L13.8668 2.52635L13.635 2.96655L13.4032 2.52635L12.959 2.29668L13.4032 2.06701L13.635 1.62681L13.8668 2.06701Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.2455 29.7042L45.8682 30.882L48.2455 32.0598L49.4341 34.4154L50.6227 32.0598L53 30.882L50.6227 29.7042L49.4341 27.3486L48.2455 29.7042Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M51.6628 30.882L50.1771 31.6182L49.4342 30.882H51.6628Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M49.4341 28.6737L50.177 30.146L49.4341 30.8821V28.6737Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M47.2054 30.8821L48.6912 30.146L49.4341 30.8821H47.2054Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M49.4341 33.0903L48.6912 31.6181L49.4341 30.882V33.0903Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M47.2054 30.882L48.6912 31.6182L49.4341 30.882H47.2054Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.4341 28.6737L48.6912 30.146L49.4341 30.8821V28.6737Z" fill="#6133B4"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M51.6628 30.8821L50.1771 30.146L49.4342 30.8821H51.6628Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.4341 33.0903L50.177 31.6181L49.4341 30.882V33.0903Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.7907 30.5287L50.4741 30.882L49.7907 31.2354L49.4341 31.9126L49.0775 31.2354L48.394 30.882L49.0775 30.5287L49.4341 29.8515L49.7907 30.5287Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.9456 43.5776L35.1911 45.9332L39.9456 48.2888L42.3228 53L44.7001 48.2888L49.4546 45.9332L44.7001 43.5776L42.3228 38.8665L39.9456 43.5776Z" fill="#6133B4"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.7803 45.9333L43.8087 47.4055L42.3229 45.9333H46.7803Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M42.3228 41.5165L43.8086 44.461L42.3228 45.9332V41.5165Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M37.8654 45.9332L40.837 44.461L42.3228 45.9332H37.8654Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M42.3228 50.35L40.837 47.4055L42.3228 45.9332V50.35Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M37.8654 45.9333L40.837 47.4055L42.3228 45.9333H37.8654Z" fill="#4F2A93"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M42.3228 41.5165L40.837 44.461L42.3228 45.9332V41.5165Z" fill="#4F2A93"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.7803 45.9332L43.8087 44.461L42.3229 45.9332H46.7803Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M42.3228 50.35L43.8086 47.4055L42.3228 45.9332V50.35Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.0359 45.2266L44.4028 45.9332L43.0359 46.6399L42.3227 47.9944L41.6096 46.6399L40.2426 45.9332L41.6096 45.2266L42.3227 43.8721L43.0359 45.2266Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |