Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cdcbc5310 | |||
| 84a52002c8 | |||
| 671c90a593 | |||
| 5d202c7617 | |||
| 5ee33f219a | |||
| 4e93874483 | |||
| 8bf44ce47a | |||
| 458bde1d13 | |||
| 3101abbb08 | |||
| 537313a21e | |||
| 453383af9f | |||
| a19db5798d | |||
| ea1569e23e | |||
| 58aa1ac2f3 | |||
| a979fc3843 | |||
| 9f91775e78 | |||
| 61ca931e66 | |||
| 2888f843e3 | |||
| b947c714f0 | |||
| 783b8995b8 | |||
| 8db2fb8015 | |||
| 1bcf2dfe80 | |||
| 805641b6cf | |||
| 5b0584fc5e | |||
| 99429d9d48 | |||
| ec5de91123 | |||
| 6ffc28f04e | |||
| b1a348aee3 | |||
| 66ed0a350b | |||
| 6b2e9f16e2 | |||
| 7f6a2a0700 | |||
| 05008b20d3 | |||
| 6cae168adb | |||
| 72a9506b9f | |||
| af96cd0488 | |||
| 60204bef06 | |||
| 4c21d9e560 | |||
| fa38b22003 | |||
| be86812900 | |||
| d03e5e93b0 | |||
| c879560445 | |||
| dc9800d88a | |||
| 39fcb3e876 | |||
| 188023b197 | |||
| e47b0982c8 | |||
| 92f217775b | |||
| daf2c354d6 | |||
| c4343379a1 | |||
| ce07d06c15 | |||
| a0041221be | |||
| 2ba6e972c3 | |||
| 6bacb89271 | |||
| ebfb6f96b3 | |||
| 22d696219a | |||
| 9014943a86 | |||
| 6a70487fa6 | |||
| 4fa381f153 | |||
| 97209e40ad | |||
| 2e97f9864e | |||
| 6d6adfd919 | |||
| 13123c0bae | |||
| 577e6f005e | |||
| 1361fea2d4 | |||
| cbcc7cd479 | |||
| 0e36c1aa0f | |||
| 5b4505ac62 | |||
| aa3d972cb4 | |||
| d4b729c95e | |||
| d4b867acc3 | |||
| 15c09691af | |||
| 27263e9b2f | |||
| 2d9715b657 | |||
| 8ca5ee99b0 | |||
| 9f9da5632d | |||
| 9364cdc2b4 | |||
| 2896cf77e0 | |||
| 936d3ffc98 | |||
| 9608b9fa9f | |||
| 657327edd7 | |||
| 9706a9c8be | |||
| 484bae40cd | |||
| c0b6353ded | |||
| f71062e86c | |||
| 8da36bf27c | |||
| 1800fabaaa | |||
| 193e7062c3 | |||
| cda5c6fbb0 | |||
| 495d01f386 | |||
| 24e1bfdfba | |||
| f757e645b7 | |||
| bf492933cc | |||
| 86d2fed76e | |||
| 2b7fe7c1d5 | |||
| 999b62df43 | |||
| cc7dac47c4 | |||
| c8189360d6 | |||
| 88183149c5 | |||
| a8f397c674 | |||
| c5aeab652d | |||
| cc04761c24 | |||
| 29dccdd148 | |||
| 186b929e59 | |||
| 551abf292c | |||
| 1e1c058d82 | |||
| 2ea9070a9b | |||
| 168a3a6e89 | |||
| 67bd2d9130 | |||
| cb0280ca8b | |||
| 259b15877b | |||
| dd31f559c7 | |||
| 6c29ea8c4c | |||
| dc316ad1c8 | |||
| a5b37fcc02 | |||
| 16f1c7286b | |||
| d5b3e4ec9f | |||
| 47eb9bf3ff | |||
| 2977346ccb | |||
| a545cc72d0 | |||
| 82548f69e5 | |||
| 5aacae3ead | |||
| 9f8162b82c | |||
| 98a749b239 | |||
| 0cef9eff87 | |||
| 3189a944d7 | |||
| 79066165e2 | |||
| c6f6df3f14 | |||
| 7bfd6ceca1 | |||
| 400f4dfb01 | |||
| 57a9fb5241 | |||
| 08c63f94fc | |||
| 9c6c90a2e9 | |||
| 39765895ee | |||
| 2a8fc7aea2 | |||
| 0a86d04a15 | |||
| 3afa7e6da5 | |||
| 0ac0723be5 | |||
| 0f2d2ddad6 | |||
| e19837f58e | |||
| 0ea1ce9758 | |||
| 9d16ab7dba | |||
| 776e9834f3 | |||
| 1d98929453 | |||
| 80664b6c5f | |||
| 01123367b1 | |||
| f6a80d18b6 | |||
| 71af306f02 | |||
| 5ccd2ae262 | |||
| a1a1fd939d | |||
| 9ac7840940 | |||
| 100275f460 | |||
| 489b9c4019 | |||
| ba61da4acb | |||
| 903851f1fd | |||
| 371a1542e7 | |||
| 0c640f07d1 | |||
| cbc6e7aa0c | |||
| b6eab67e6a | |||
| 99afefe953 | |||
| 9fea01b1f4 | |||
| 978b78e57a | |||
| f920a441a5 | |||
| 12a9eec540 | |||
| f83d86b7f3 | |||
| 0410c97001 | |||
| 4482b734a5 | |||
| 0131cc07bf | |||
| 70a9f66dcd | |||
| 3057fdbd4a | |||
| cc48479c66 | |||
| 8ad644ec3a | |||
| 6f2bf5659d | |||
| f4e573f684 | |||
| a2e59d0920 | |||
| 39fe86c688 | |||
| 78ceb427a3 | |||
| 7c17a32bbb | |||
| 9a34c16fa2 | |||
| 38c24763fa | |||
| 776f3d288b | |||
| b7448e2cfe | |||
| 0bc836b490 | |||
| fdf7e3a665 | |||
| 00d12e83bd | |||
| e4661c3763 | |||
| 5fcf3fba88 | |||
| 156d15c540 | |||
| 7d9b8a5ceb | |||
| 23b19853b4 | |||
| c6839c4478 | |||
| 167f4f07b8 | |||
| 95c9dbaa73 | |||
| c88af182c9 | |||
| d9f7772453 | |||
| 931d8814b6 | |||
| 25e5183370 | |||
| 8e559da200 | |||
| 8efed37241 | |||
| ed25afb0b1 | |||
| a2c91aae70 | |||
| 3ac69d5e75 | |||
| 87b26c4cfb | |||
| 28bc843779 | |||
| e92ff9737a | |||
| 66422d4235 | |||
| c3343c9412 | |||
| e9100c7132 | |||
| 11235685ca | |||
| 49d6691f7d | |||
| db4c4e6493 | |||
| 56b1f6371f | |||
| 1b5bd8e1ab | |||
| e39eafd3f0 | |||
| 9213181ca2 | |||
| 3e50469ed5 | |||
| 7e9d3062f3 | |||
| 6376e57614 | |||
| 84ad270436 | |||
| 92cf506bad | |||
| 5f97cb31b8 | |||
| 6d26fbc5f2 | |||
| bb9912de89 | |||
| 7a51b7593f | |||
| 1bfc55ece1 | |||
| e21f64edaf | |||
| a08b419411 | |||
| 4a752c3347 | |||
| 8609aae1b4 | |||
| d35fd9d90d | |||
| 11103813f5 | |||
| 1a75c6a696 | |||
| 6bef105cf6 | |||
| 3fffe7aa5c | |||
| 9ab9b0f553 | |||
| a00add46a7 | |||
| b9d8da44de | |||
| 947e8a1836 | |||
| 7bdc974704 | |||
| fe8780d49c | |||
| 2fc4d0f00c | |||
| eb4e382ecf | |||
| 4300c7b1bf | |||
| 2cd0ed5973 | |||
| 6e8bdf4cdf | |||
| 0bac1102cc | |||
| 3e96e54ad8 | |||
| 3458d89c1d | |||
| 25e72ad907 | |||
| 5cf6a67a36 | |||
| 9dcce382a3 | |||
| f6484c872a | |||
| 249ba77c01 | |||
| 7ff590cd88 | |||
| f297fef89e | |||
| b037cb0722 | |||
| b8c58a7e4f | |||
| f973bf1038 | |||
| aaea985cf2 | |||
| 1d0e08419f | |||
| fd6244eb15 | |||
| f8aa756d52 | |||
| ae7df804cb | |||
| de37eb1bb2 | |||
| cf03261bbf | |||
| 3ec95ad821 | |||
| 57d11d5b20 | |||
| 039e7d40b8 | |||
| 4389a9b478 | |||
| 289032047c | |||
| 6f5515214a | |||
| fd2c4e3265 | |||
| dd91bada8f | |||
| d724933640 | |||
| e4b74bc347 | |||
| c609db09c1 | |||
| 55feebdf9e | |||
| d8a99647e7 | |||
| 353b4aed05 | |||
| 411f82202b | |||
| 5a5a6e4c5d | |||
| 914eee015e | |||
| a301f817e9 | |||
| 519af8f1b6 | |||
| a71abea032 | |||
| 3c623b08c4 | |||
| ddfa3f8a91 | |||
| e2d1de0cf0 | |||
| 9281de1801 | |||
| 4960171565 | |||
| d063a57faa | |||
| 0960eaf571 | |||
| fd0ec41c53 | |||
| 1420e1c8d7 | |||
| c7c854664f | |||
| 68e5679340 | |||
| 32a9dda2c6 | |||
| 8b19c0ad69 | |||
| 33e8b64df6 | |||
| 427251ed1d |
@@ -37,6 +37,7 @@ yarn.lock
|
||||
.elasticbeanstalk/*
|
||||
!.elasticbeanstalk/*.cfg.yml
|
||||
!.elasticbeanstalk/*.global.yml
|
||||
/.vscode
|
||||
|
||||
# webstorm fake webpack for path intellisense
|
||||
webpack.webstorm.config
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
node_modules/**
|
||||
content_cache
|
||||
content_cache/**
|
||||
website/client/**
|
||||
test/**
|
||||
.git/**
|
||||
@@ -7,3 +9,4 @@ test/**
|
||||
*.swp
|
||||
*.swx
|
||||
website/raw_sprites/**
|
||||
content_cache/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
FROM node:12
|
||||
|
||||
ENV ADMIN_EMAIL admin@habitica.com
|
||||
ENV EMAILS_COMMUNITY_MANAGER_EMAIL admin@habitica.com
|
||||
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
|
||||
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
|
||||
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
|
||||
@@ -11,6 +12,7 @@ ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleu
|
||||
ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a
|
||||
ENV NODE_ENV production
|
||||
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
|
||||
ENV APPLE_AUTH_CLIENT_ID 9Q9SMRMCNN.com.habitrpg.ios.Habitica
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
* Code is GPL v3 licensed:
|
||||
This Source Code is subject to the terms of the GNU General Public License, v. 3.0.
|
||||
If a copy of the GPL was not distributed with this file, You can obtain one at http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
If a copy of the GPL was not distributed with this file, you can obtain one at http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
* Assets and content designed for Mozilla BrowserQuest are licensed under CC-BY-SA 3.0:
|
||||
http://creativecommons.org/licenses/by-sa/3.0/
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost/habitrpg",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitrpg",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
@@ -70,9 +70,15 @@
|
||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"TEST_DB_URI": "mongodb://localhost/habitrpg_test",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitrpg_test",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false"
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import gulp from 'gulp';
|
||||
import babel from 'gulp-babel';
|
||||
|
||||
gulp.task('build:src', () => gulp.src('website/server/**/*.js')
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
.pipe(babel())
|
||||
.pipe(gulp.dest('website/transpiled-babel/')));
|
||||
|
||||
gulp.task('build:common', () => gulp.src('website/common/script/**/*.js')
|
||||
gulp.task('build:babel:common', () => gulp.src('website/common/script/**/*.js')
|
||||
.pipe(babel())
|
||||
.pipe(gulp.dest('website/common/transpiled-babel/')));
|
||||
|
||||
gulp.task('build:server', gulp.series('build:src', 'build:common', done => done()));
|
||||
gulp.task('build:babel', gulp.parallel('build:babel:server', 'build:babel:common', done => done()));
|
||||
|
||||
gulp.task('build:prod', gulp.series(
|
||||
'build:server',
|
||||
'build:babel',
|
||||
'apidoc',
|
||||
'content:cache',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// TODO parallelize, use gulp file helpers
|
||||
gulp.task('content:cache', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
fs.mkdirSync(CONTENT_CACHE_PATH);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
|
||||
// clone the content for each language and save
|
||||
// localize it
|
||||
// save the result
|
||||
langCodes.forEach(langCode => {
|
||||
fs.writeFileSync(
|
||||
`${CONTENT_CACHE_PATH}${langCode}.json`,
|
||||
getLocalizedContentResponse(langCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
@@ -161,4 +161,4 @@ gulp.task('sprites:checkCompiledDimensions', gulp.series('sprites:main', 'sprite
|
||||
done();
|
||||
}));
|
||||
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions', done => done()));
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:checkCompiledDimensions', done => done()));
|
||||
|
||||
@@ -13,8 +13,16 @@ const gulp = require('gulp');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20200402_webhooks_add_protocol';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.webhooks && user.webhooks.length > 0) {
|
||||
user.webhooks.forEach(webhook => {
|
||||
// Make sure the protocol is set and valid
|
||||
if (webhook.url.startsWith('ftp')) {
|
||||
webhook.url = webhook.url.replace('ftp', 'https');
|
||||
}
|
||||
|
||||
if (!webhook.url.startsWith('http://') && !webhook.url.startsWith('https://')) {
|
||||
// the default in got 9 was https
|
||||
// see https://github.com/sindresorhus/got/commit/92bc8082137d7d085750359bbd76c801e213d7d2#diff-0730bb7c2e8f9ea2438b52e419dd86c9L111
|
||||
webhook.url = `https://${webhook.url}`;
|
||||
}
|
||||
});
|
||||
|
||||
set.webhooks = user.webhooks;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
webhooks: { $exists: true, $not: { $size: 0 } },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
webhooks: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20200402_webhooks_reenable';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.webhooks && user.webhooks.length > 0) {
|
||||
user.webhooks.forEach(webhook => {
|
||||
// Re-enable webhooks disabled because of too many failures
|
||||
if (webhook.enabled === false && webhook.lastFailureAt === null) {
|
||||
webhook.enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
set.webhooks = user.webhooks;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
webhooks: { $exists: true, $not: { $size: 0 } },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
webhooks: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,7 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
const processUsers = () => {}; // require('').default;
|
||||
const processUsers = require().default;
|
||||
|
||||
processUsers()
|
||||
.then(() => {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// @migrationName = 'RewardsMigrationFlipNegativeCostsValues';
|
||||
// @authorName = 'hamboomger';
|
||||
// @authorUuid = '80b61b73-2278-4947-b713-a10112cfe7f5';
|
||||
|
||||
/*
|
||||
* For each reward with negative cost, make it positive
|
||||
* by assigning it an absolute value of itself
|
||||
*/
|
||||
|
||||
import { Task } from '../../website/server/models/task';
|
||||
|
||||
async function flipNegativeCostsValues () {
|
||||
const query = {
|
||||
type: 'reward',
|
||||
value: { $lt: 0 },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
value: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const rewards = await Task
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({ _id: 1 })
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (rewards.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const promises = rewards.map(reward => {
|
||||
const positiveValue = Math.abs(reward.value);
|
||||
return Task.update({ _id: reward._id }, { $set: { value: positiveValue } }).exec();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(promises);
|
||||
|
||||
query._id = {
|
||||
$gt: rewards[rewards.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('All rewards with negative values were updated, migration finished');
|
||||
}
|
||||
|
||||
export default flipNegativeCostsValues;
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable no-console */
|
||||
import each from 'lodash/each';
|
||||
import keys from 'lodash/keys';
|
||||
import content from '../../website/common/script/content/index';
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = 'full-gear';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* Award users every extant pet and mount
|
||||
*/
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
each(keys(content.gear.flat), gearItem => {
|
||||
set[`items.gear.owned.${gearItem}`] = true;
|
||||
});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.lowerCaseUsername': 'olson1',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 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
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ async function updateUser (user) {
|
||||
each(keys(content.specialPets), pet => {
|
||||
set[`items.pets.${pet}`] = 5;
|
||||
});
|
||||
each(keys(content.wackyPets), pet => {
|
||||
set[`items.pets.${pet}`] = 5;
|
||||
});
|
||||
each(keys(content.mounts), mount => {
|
||||
set[`items.mounts.${mount}`] = true;
|
||||
});
|
||||
@@ -54,7 +57,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.username': 'olson22',
|
||||
'auth.local.username': 'SabreTest',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.137.0",
|
||||
"version": "4.140.14",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/preset-env": "^7.8.7",
|
||||
"@babel/register": "^7.8.6",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/register": "^7.9.0",
|
||||
"@google-cloud/trace-agent": "^4.2.5",
|
||||
"@slack/client": "^3.8.1",
|
||||
"@slack/client": "^4.12.0",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.8",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"aws-sdk": "^2.635.0",
|
||||
"apple-auth": "^1.0.5",
|
||||
"bcrypt": "^3.0.8",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.4.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.3.6",
|
||||
"csv-stringify": "^5.4.3",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -30,35 +30,36 @@
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.1.6",
|
||||
"got": "^9.0.0",
|
||||
"got": "^10.7.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^6.2.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"helmet": "^3.21.3",
|
||||
"habitica-markdown": "^1.4.0",
|
||||
"helmet": "^3.22.0",
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.8.0",
|
||||
"lodash": "^4.17.15",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.3",
|
||||
"morgan": "^1.7.0",
|
||||
"mongoose": "^5.9.10",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
"pageres": "^5.1.0",
|
||||
"on-headers": "^1.0.2",
|
||||
"passport": "^0.4.1",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"ps-tree": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
@@ -70,7 +71,7 @@
|
||||
"validator": "^11.0.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.2.1",
|
||||
"winston-loggly-bulk": "^3.0.1",
|
||||
"winston-loggly-bulk": "^3.1.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"private": true,
|
||||
@@ -99,6 +100,7 @@
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"postinstall": "gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
@@ -110,7 +112,7 @@
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.1.2",
|
||||
"monk": "^7.2.0",
|
||||
"require-again": "^2.0.0",
|
||||
"sinon": "^7.2.4",
|
||||
"sinon-chai": "^3.5.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as contentLib from '../../../../website/server/libs/content';
|
||||
import content from '../../../../website/common/script/content';
|
||||
|
||||
describe('contentLib', () => {
|
||||
describe('CONTENT_CACHE_PATH', () => {
|
||||
it('exports CONTENT_CACHE_PATH', () => {
|
||||
expect(contentLib.CONTENT_CACHE_PATH).to.be.a.string;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalizedContentResponse', () => {
|
||||
it('clones, not modify, the original content data', () => {
|
||||
contentLib.getLocalizedContentResponse();
|
||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,16 @@ function getUser () {
|
||||
value: 'email@facebook',
|
||||
}],
|
||||
},
|
||||
google: {
|
||||
emails: [{
|
||||
value: 'email@google',
|
||||
}],
|
||||
},
|
||||
apple: {
|
||||
emails: [{
|
||||
value: 'email@apple',
|
||||
}],
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: 'profile name',
|
||||
@@ -58,6 +68,8 @@ describe('emails', () => {
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
@@ -67,12 +79,48 @@ describe('emails', () => {
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [google users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.google.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [apple users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.facebook.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.apple.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('has fallbacks for missing data', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook;
|
||||
delete user.auth.google;
|
||||
delete user.auth.apple;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
@@ -121,8 +169,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
@@ -154,8 +201,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
@@ -177,8 +223,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CustomError,
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
Forbidden,
|
||||
InternalServerError,
|
||||
NotFound,
|
||||
NotificationNotFound,
|
||||
@@ -113,6 +114,32 @@ describe('Custom Errors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Forbidden', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 401', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError.httpCode).to.eql(403);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError.message).to.eql('Access forbidden.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
const forbiddenError = new Forbidden('Custom Error Message');
|
||||
|
||||
expect(forbiddenError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('InternalServerError', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
const internalServerError = new InternalServerError();
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import mongoose from 'mongoose';
|
||||
import {
|
||||
highlightMentions,
|
||||
} from '../../../../website/server/libs/highlightMentions';
|
||||
|
||||
describe('highlightMentions', () => {
|
||||
beforeEach(() => {
|
||||
const mockFind = {
|
||||
select () {
|
||||
return this;
|
||||
},
|
||||
lean () {
|
||||
return this;
|
||||
},
|
||||
exec () {
|
||||
return Promise.resolve([{
|
||||
auth: { local: { username: 'user' } }, _id: '111',
|
||||
}, { auth: { local: { username: 'user2' } }, _id: '222' }, { auth: { local: { username: 'user3' } }, _id: '333' }, { auth: { local: { username: 'user-dash' } }, _id: '444' }, { auth: { local: { username: 'user_underscore' } }, _id: '555' },
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
sinon.stub(mongoose.Model, 'find').returns(mockFind);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('doesn\'t change text without mentions', async () => {
|
||||
const text = 'some chat text';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
it('highlights existing users', async () => {
|
||||
const text = '@user: message';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user](/profile/111): message');
|
||||
});
|
||||
it('highlights special characters', async () => {
|
||||
const text = '@user-dash: message @user_underscore';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||
});
|
||||
it('doesn\'t highlight nonexisting users', async () => {
|
||||
const text = '@nouser message';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('@nouser message');
|
||||
});
|
||||
it('highlights multiple existing users', async () => {
|
||||
const text = '@user message (@user2) @user3 @user';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user](/profile/111) message ([@user2](/profile/222)) [@user3](/profile/333) [@user](/profile/111)');
|
||||
});
|
||||
it('doesn\'t highlight more than 5 users', async () => {
|
||||
const text = '@user @user2 @user3 @user4 @user5 @user6';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import mongoose from 'mongoose';
|
||||
import highlightMentions from '../../../../website/server/libs/highlightMentions';
|
||||
|
||||
describe('highlightMentions', () => {
|
||||
beforeEach(() => {
|
||||
const mockFind = {
|
||||
select () {
|
||||
return this;
|
||||
},
|
||||
lean () {
|
||||
return this;
|
||||
},
|
||||
exec () {
|
||||
return Promise.resolve([
|
||||
{ auth: { local: { username: 'user' } }, _id: '111' },
|
||||
{ auth: { local: { username: 'user2' } }, _id: '222' },
|
||||
{ auth: { local: { username: 'user3' } }, _id: '333' },
|
||||
{ auth: { local: { username: 'user-dash' } }, _id: '444' },
|
||||
{ auth: { local: { username: 'user_underscore' } }, _id: '555' },
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
sinon.stub(mongoose.Model, 'find').returns(mockFind);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('doesn\'t change text without mentions', async () => {
|
||||
const text = 'some chat text';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('highlights existing users', async () => {
|
||||
const text = '@user: message';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user](/profile/111): message');
|
||||
});
|
||||
|
||||
it('highlights special characters', async () => {
|
||||
const text = '@user-dash: message @user_underscore';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||
});
|
||||
|
||||
it('doesn\'t highlight nonexisting users', async () => {
|
||||
const text = '@nouser message';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('@nouser message');
|
||||
});
|
||||
|
||||
it('highlights multiple existing users', async () => {
|
||||
const text = '@user message (@user2) @user3 @user';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@user](/profile/111) message ([@user2](/profile/222)) [@user3](/profile/333) [@user](/profile/111)');
|
||||
});
|
||||
|
||||
it('doesn\'t highlight more than 5 users', async () => {
|
||||
const text = '@user @user2 @user3 @user4 @user5 @user6';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
describe('exceptions in code blocks', () => {
|
||||
it('doesn\'t highlight user in inline code block', async () => {
|
||||
const text = '`@user`';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight user in fenced code block', async () => {
|
||||
const text = 'Text\n\n```\n// code referencing @user\n```\n\nText';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight user in indented code block', async () => {
|
||||
const text = ' @user';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('does highlight user that\'s after in-line code block', async () => {
|
||||
const text = '`<code />` for @user';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal('`<code />` for [@user](/profile/111)');
|
||||
});
|
||||
|
||||
it('does highlight same content properly', async () => {
|
||||
const text = '@user `@user`';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal('[@user](/profile/111) `@user`');
|
||||
});
|
||||
});
|
||||
|
||||
it('github issue 12118, method crashes when square brackets are used', async () => {
|
||||
const text = '[test]';
|
||||
|
||||
let err;
|
||||
|
||||
try {
|
||||
await highlightMentions(text);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).to.be.undefined;
|
||||
});
|
||||
|
||||
it('github issue 12138, method crashes when regex chars are used in code block', async () => {
|
||||
const text = '`[test]`';
|
||||
|
||||
let err;
|
||||
|
||||
try {
|
||||
await highlightMentions(text);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).to.be.undefined;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
getLanguageFromBrowser,
|
||||
getLanguageFromUser,
|
||||
} from '../../../../website/server/libs/language';
|
||||
import {
|
||||
generateReq,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('language lib', () => {
|
||||
let req;
|
||||
|
||||
beforeEach(() => {
|
||||
req = generateReq();
|
||||
});
|
||||
|
||||
describe('getLanguageFromUser', () => {
|
||||
it('uses the user preferred language if avalaible', () => {
|
||||
const user = {
|
||||
preferences: {
|
||||
language: 'it',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getLanguageFromUser(user, req)).to.equal('it');
|
||||
});
|
||||
|
||||
it('falls back to english if the user preferred language is not avalaible', () => {
|
||||
const user = {
|
||||
preferences: {
|
||||
language: 'bla',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getLanguageFromUser(user, req)).to.equal('en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLanguageFromBrowser', () => {
|
||||
it('uses browser specificed language', () => {
|
||||
req.headers['accept-language'] = 'pt';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('pt');
|
||||
});
|
||||
|
||||
it('uses first language in series if browser specifies multiple', () => {
|
||||
req.headers['accept-language'] = 'he, pt, it';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('he');
|
||||
});
|
||||
|
||||
it('skips invalid lanaguages and uses first language in series if browser specifies multiple', () => {
|
||||
req.headers['accept-language'] = 'blah, he, pt, it';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('he');
|
||||
});
|
||||
|
||||
it('uses normal version of language if specialized locale is passed in', () => {
|
||||
req.headers['accept-language'] = 'fr-CA';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('fr');
|
||||
});
|
||||
|
||||
it('uses normal version of language if specialized locale is passed in', () => {
|
||||
req.headers['accept-language'] = 'fr-CA';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('fr');
|
||||
});
|
||||
|
||||
it('uses es if es is passed in', () => {
|
||||
req.headers['accept-language'] = 'es';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('es');
|
||||
});
|
||||
|
||||
it('uses es_419 if applicable es-languages are passed in', () => {
|
||||
req.headers['accept-language'] = 'es-mx';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('es_419');
|
||||
});
|
||||
|
||||
it('uses es_419 if multiple es languages are passed in', () => {
|
||||
req.headers['accept-language'] = 'es-GT, es-MX, es-CR';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('es_419');
|
||||
});
|
||||
|
||||
it('zh', () => {
|
||||
req.headers['accept-language'] = 'zh-TW';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('zh_TW');
|
||||
});
|
||||
|
||||
it('uses english if browser specified language is not compatible', () => {
|
||||
req.headers['accept-language'] = 'blah';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('en');
|
||||
});
|
||||
|
||||
it('uses english if browser does not specify', () => {
|
||||
req.headers['accept-language'] = '';
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('en');
|
||||
});
|
||||
|
||||
it('uses english if browser does not supply an accept-language header', () => {
|
||||
delete req.headers['accept-language'];
|
||||
|
||||
expect(getLanguageFromBrowser(req)).to.equal('en');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import faq from '../../../../website/common/script/content/faq';
|
||||
import common from '../../../../website/common';
|
||||
import { localizeContentData } from '../../../../website/server/libs/content';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('localizeContentData', () => {
|
||||
it('Should take a an object with localization identifiers and '
|
||||
+ 'return an object with actual translations in English', () => {
|
||||
const faqInEnglish = localizeContentData(faq, 'en');
|
||||
|
||||
expect(faqInEnglish).to.have.property('stillNeedHelp');
|
||||
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'en'));
|
||||
});
|
||||
it('Should take an object with localization identifiers and '
|
||||
+ 'return an object with actual translations in German', () => {
|
||||
const faqInEnglish = localizeContentData(faq, 'de');
|
||||
|
||||
expect(faqInEnglish).to.have.property('stillNeedHelp');
|
||||
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
|
||||
});
|
||||
});
|
||||
@@ -30,18 +30,52 @@ describe('logger', () => {
|
||||
|
||||
describe('info', () => {
|
||||
it('calls winston\'s info log', () => {
|
||||
logger.info(1, 2, 3);
|
||||
logger.info('1', 2);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWith(1, 2, 3);
|
||||
expect(infoSpy).to.be.calledWith('1', { extraData: 2 });
|
||||
});
|
||||
|
||||
it('allows up to two arguments', () => {
|
||||
expect(() => logger.info('1', 2, 3)).to.throw;
|
||||
expect(infoSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('has default message', () => {
|
||||
logger.info(1);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWith('No message provided for log.', { extraData: 1 });
|
||||
});
|
||||
|
||||
it('wraps non objects', () => {
|
||||
logger.info('message', [1, 2]);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWithMatch('message', { extraData: [1, 2] });
|
||||
});
|
||||
|
||||
it('does not wrap objects', () => {
|
||||
logger.info('message', { a: 1, b: 2 });
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWithMatch('message', { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('throws if two arguments and no message', () => {
|
||||
expect(() => logger.info({ a: 1 }, { b: 2 })).to.throw;
|
||||
expect(infoSpy).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
context('non-error object', () => {
|
||||
it('passes through arguments if the first arg is not an error object', () => {
|
||||
logger.error(1, 2, 3, 4);
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(1, 2, 3, 4);
|
||||
it('allows up to two arguments', () => {
|
||||
expect(() => logger.error('1', 2, 3)).to.throw;
|
||||
expect(errorSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('handled non-error object', () => {
|
||||
logger.error(1, 2);
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWithMatch('logger.error expects an Error instance', {
|
||||
invalidErr: 1,
|
||||
extraData: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,14 +84,12 @@ describe('logger', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -68,56 +100,60 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
fullError: anotherError,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: anotherError },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is null', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, null, 2, 3);
|
||||
logger.error(errInstance, null);
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
null,
|
||||
2,
|
||||
3,
|
||||
{ },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is not an object', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, true, 2, 3);
|
||||
logger.error(errInstance, true);
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
true,
|
||||
2,
|
||||
3,
|
||||
{ extraData: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is a string', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, 'a string');
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
{ extraMessage: 'a string' },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData does not include isHandledError property', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, { httpCode: 400 }, 2, 3);
|
||||
logger.error(errInstance, { httpCode: 400 });
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 400, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -127,14 +163,12 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 502,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 502, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -144,14 +178,12 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 403,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(warnSpy).to.be.calledOnce;
|
||||
expect(warnSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 403, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,7 +192,7 @@ describe('logger', () => {
|
||||
|
||||
errInstance.customField = 'Some interesting data';
|
||||
|
||||
logger.error(errInstance, {}, 2, 3);
|
||||
logger.error(errInstance, {});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
@@ -170,8 +202,6 @@ describe('logger', () => {
|
||||
customField: 'Some interesting data',
|
||||
},
|
||||
},
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
@@ -293,7 +292,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
@@ -308,26 +307,46 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'admin-user-subscription-details';
|
||||
});
|
||||
expect(adminUserSubscriptionDetails).to.exist;
|
||||
expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === recipient._id;
|
||||
});
|
||||
expect(groupMemberJoinOne).to.exist;
|
||||
expect(groupMemberJoinOne[0]._id).to.equal(recipient._id);
|
||||
expect(groupMemberJoinOne[2]).to.eql([
|
||||
{ name: 'LEADER', content: groupLeaderName },
|
||||
{ name: 'GROUP_NAME', content: groupName },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE },
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
|
||||
const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === group.leader;
|
||||
});
|
||||
expect(groupMemberJoinTwo).to.exist;
|
||||
expect(groupMemberJoinTwo[0]._id).to.equal(group.leader);
|
||||
|
||||
const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'group-subscription-begins';
|
||||
});
|
||||
expect(groupSubscriptionBegins).to.exist;
|
||||
expect(groupSubscriptionBegins[0]._id).to.equal(group.leader);
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
@@ -342,22 +361,43 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
|
||||
const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'admin-user-subscription-details';
|
||||
});
|
||||
expect(adminUserSubscriptionDetails).to.exist;
|
||||
expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === recipient._id;
|
||||
});
|
||||
expect(groupMemberJoinOne).to.exist;
|
||||
expect(groupMemberJoinOne[0]._id).to.equal(recipient._id);
|
||||
expect(groupMemberJoinOne[2]).to.eql([
|
||||
{ name: 'LEADER', content: groupLeaderName },
|
||||
{ name: 'GROUP_NAME', content: groupName },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS },
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
|
||||
const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === group.leader;
|
||||
});
|
||||
expect(groupMemberJoinTwo).to.exist;
|
||||
expect(groupMemberJoinTwo[0]._id).to.equal(group.leader);
|
||||
|
||||
const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'group-subscription-begins';
|
||||
});
|
||||
expect(groupSubscriptionBegins).to.exist;
|
||||
expect(groupSubscriptionBegins[0]._id).to.equal(group.leader);
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import moment from 'moment';
|
||||
import { calculateSubscriptionTerminationDate } from '../../../../../../website/server/libs/payments/util';
|
||||
import api from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
describe('#calculateSubscriptionTerminationDate', () => {
|
||||
let plan;
|
||||
let nextBill;
|
||||
|
||||
beforeEach(() => {
|
||||
plan = {
|
||||
customerId: 'customer-id',
|
||||
extraMonths: 0,
|
||||
};
|
||||
nextBill = moment();
|
||||
});
|
||||
it('should extend date to the exact amount of days left before the next bill will occur', () => {
|
||||
nextBill = moment()
|
||||
.add(5, 'days');
|
||||
const expectedTerminationDate = moment()
|
||||
.add(5, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
it('if nextBill is null, add 30 days to termination date', () => {
|
||||
nextBill = null;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(30, 'days');
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
it('if nextBill is null and it\'s a group plan, add 2 days instead of 30', () => {
|
||||
nextBill = null;
|
||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(2, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
it('should add 30.5 days for each extraMonth', () => {
|
||||
plan.extraMonths = 4;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(30.5 * 4, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
it('should round up if total days gained by extraMonth is a decimal number', () => {
|
||||
plan.extraMonths = 5;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(Math.ceil(30.5 * 5), 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
it('behaves like extraMonths is 0 if it\'s set to a negative number', () => {
|
||||
plan.extraMonths = -5;
|
||||
const expectedTerminationDate = moment();
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import requireAgain from 'require-again';
|
||||
import apn from 'apn/mock';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import {
|
||||
sendNotification as sendPushNotification,
|
||||
MAX_MESSAGE_LENGTH,
|
||||
} from '../../../../website/server/libs/pushNotifications';
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let sendPushNotification;
|
||||
const pathToPushNotifications = '../../../../website/server/libs/pushNotifications';
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
|
||||
@@ -28,8 +30,6 @@ describe('pushNotifications', () => {
|
||||
on: () => null,
|
||||
send: apnSendSpy,
|
||||
});
|
||||
|
||||
sendPushNotification = requireAgain(pathToPushNotifications).sendNotification;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -86,6 +86,67 @@ describe('pushNotifications', () => {
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
payload: {
|
||||
message: longMessage,
|
||||
},
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.payload.message)
|
||||
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars (no payload)', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
// TODO disabled because APN relies on a Promise
|
||||
xit('uses APN for iOS devices', () => {
|
||||
user.pushDevices.push({
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getMatchesByWordArray } from '../../../../website/server/libs/stringUtils';
|
||||
import bannedWords from '../../../../website/server/libs/bannedWords';
|
||||
|
||||
describe('stringUtils', () => {
|
||||
describe('getMatchesByWordArray', () => {
|
||||
it('check all banned words are matched', async () => {
|
||||
const message = bannedWords.join(',').replace(/\\/g, '');
|
||||
const matches = getMatchesByWordArray(message, bannedWords);
|
||||
expect(matches.length).to.equal(bannedWords.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import got from 'got';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
@@ -13,8 +14,9 @@ import {
|
||||
import {
|
||||
generateUser,
|
||||
defer,
|
||||
sleep,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks; let
|
||||
@@ -99,8 +101,7 @@ describe('webhooks', () => {
|
||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body,
|
||||
json: body,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +121,7 @@ describe('webhooks', () => {
|
||||
expect(sendWebhook.attachDefaultData).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
json: body,
|
||||
});
|
||||
|
||||
expect(body).to.eql({
|
||||
@@ -151,8 +152,7 @@ describe('webhooks', () => {
|
||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
foo: 'bar',
|
||||
baz: 'biz',
|
||||
},
|
||||
@@ -269,8 +269,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
json: body,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -290,8 +289,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
json: body,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,12 +312,105 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledTwice;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
json: body,
|
||||
});
|
||||
expect(got.post).to.be.calledWithMatch('http://other-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
json: body,
|
||||
});
|
||||
});
|
||||
|
||||
describe('failures', () => {
|
||||
let sendWebhook;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox.restore();
|
||||
sandbox.stub(got, 'post').returns(Promise.reject());
|
||||
|
||||
sendWebhook = new WebhookSender({ type: 'taskActivity' });
|
||||
user.webhooks = [{
|
||||
url: 'http://custom-url.com', enabled: true, type: 'taskActivity',
|
||||
}];
|
||||
await user.save();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
expect(user.webhooks[0].lastFailureAt).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not increase failures counter if request is successfull', async () => {
|
||||
sandbox.restore();
|
||||
sandbox.stub(got, 'post').returns(Promise.resolve());
|
||||
|
||||
const body = {};
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: body,
|
||||
});
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
expect(user.webhooks[0].lastFailureAt).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('records failures', async () => {
|
||||
sinon.stub(logger, 'error');
|
||||
const body = {};
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: body,
|
||||
});
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(1);
|
||||
expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true;
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
logger.error.restore();
|
||||
});
|
||||
|
||||
it('disables a webhook after 10 failures', async () => {
|
||||
const times = 10;
|
||||
for (let i = 0; i < times; i += 1) {
|
||||
sendWebhook.send(user, {});
|
||||
await sleep(0.1); // eslint-disable-line no-await-in-loop
|
||||
user = await User.findById(user._id).exec(); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
expect(got.post).to.be.callCount(10);
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].enabled).to.equal(false);
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets failures after a month ', async () => {
|
||||
const oneMonthAgo = moment().subtract(1, 'months').subtract(1, 'days').toDate();
|
||||
user.webhooks[0].lastFailureAt = oneMonthAgo;
|
||||
user.webhooks[0].failures = 9;
|
||||
|
||||
await user.save();
|
||||
|
||||
sendWebhook.send(user, []);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(1);
|
||||
// Check that the stored date is whitin 10s from now
|
||||
expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -364,8 +455,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
type: 'scored',
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
@@ -402,8 +492,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://global-activity.com', {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
type: 'scored',
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
@@ -456,8 +545,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
type,
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
@@ -497,8 +585,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
@@ -538,8 +625,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[2].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
type,
|
||||
webhookType: 'userActivity',
|
||||
user: {
|
||||
@@ -585,8 +671,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[1].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
type,
|
||||
webhookType: 'questActivity',
|
||||
user: {
|
||||
@@ -632,8 +717,7 @@ describe('webhooks', () => {
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
|
||||
json: true,
|
||||
body: {
|
||||
json: {
|
||||
webhookType: 'groupChatReceived',
|
||||
user: {
|
||||
_id: user._id,
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('analytics middleware', () => {
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches analytics object res.locals', () => {
|
||||
it('attaches analytics object to res', () => {
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import {
|
||||
disableCache,
|
||||
} from '../../../../website/server/middlewares/cache';
|
||||
|
||||
describe('cache middlewares', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = generateReq();
|
||||
res = generateRes();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
describe('disableCache', () => {
|
||||
it('sets the correct headers', () => {
|
||||
disableCache(req, res, next);
|
||||
expect(res.set).to.have.been.calledWith('Cache-Control', 'no-store');
|
||||
expect(next).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
xit('removes the etag header', () => {
|
||||
// @TODO how to stub onHeaders
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,28 +21,11 @@ describe('cron middleware', () => {
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(done => {
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@email.email',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
user.save()
|
||||
.then(savedUser => {
|
||||
res.locals.user = savedUser;
|
||||
res.analytics = analyticsService;
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analyticsService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -170,6 +170,7 @@ describe('errorHandler', () => {
|
||||
originalUrl: req.originalUrl,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
httpCode: 400,
|
||||
isHandledError: true,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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.headers['x-forwarded-for'] = '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 a matching ip exist in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '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);
|
||||
});
|
||||
|
||||
it('trims ips in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(', 192.168.1.1 , 192.168.1.4, ');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
|
||||
it('works when multiple ips are passed in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.4';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1, 192.168.1.4, 192.168.1.3');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,9 @@ import { model as User } from '../../../../website/server/models/user';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
// TODO some of the checks here can be simplified to simply check
|
||||
// that the right parameters are passed to the functions in libs/language
|
||||
|
||||
describe('language middleware', () => {
|
||||
describe('res.t', () => {
|
||||
let res; let req; let
|
||||
@@ -19,6 +22,8 @@ describe('language middleware', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
// remove the defaul user
|
||||
res.locals.user = undefined;
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
|
||||
@@ -57,6 +62,8 @@ describe('language middleware', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
// remove the defaul user
|
||||
res.locals.user = undefined;
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
attachTranslateFunction(req, res, next);
|
||||
@@ -88,7 +95,7 @@ describe('language middleware', () => {
|
||||
lang: 'es',
|
||||
};
|
||||
|
||||
req.locals = {
|
||||
res.locals = {
|
||||
user: {
|
||||
preferences: {
|
||||
language: 'it',
|
||||
@@ -108,7 +115,7 @@ describe('language middleware', () => {
|
||||
|
||||
context('authorized request', () => {
|
||||
it('uses the user preferred language if avalaible', () => {
|
||||
req.locals = {
|
||||
res.locals = {
|
||||
user: {
|
||||
preferences: {
|
||||
language: 'it',
|
||||
@@ -122,7 +129,7 @@ describe('language middleware', () => {
|
||||
});
|
||||
|
||||
it('falls back to english if the user preferred language is not avalaible', done => {
|
||||
req.locals = {
|
||||
res.locals = {
|
||||
user: {
|
||||
preferences: {
|
||||
language: 'bla',
|
||||
@@ -138,7 +145,7 @@ describe('language middleware', () => {
|
||||
});
|
||||
|
||||
it('uses the user preferred language even if a session is included in request', () => {
|
||||
req.locals = {
|
||||
res.locals = {
|
||||
user: {
|
||||
preferences: {
|
||||
language: 'it',
|
||||
|
||||
@@ -538,52 +538,74 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a chat message if no progress is made on quest with multiple items', async () => {
|
||||
progress.collectedItems = 0;
|
||||
party.quest.key = 'dilatoryDistress1';
|
||||
party.quest.active = false;
|
||||
describe('collection quests with multiple items', () => {
|
||||
it('sends a chat message if no progress is made on quest with multiple items', async () => {
|
||||
progress.collectedItems = 0;
|
||||
party.quest.key = 'dilatoryDistress1';
|
||||
party.quest.active = false;
|
||||
|
||||
await party.startQuest(questLeader);
|
||||
Group.prototype.sendChat.resetHistory();
|
||||
await party.save();
|
||||
await party.startQuest(questLeader);
|
||||
Group.prototype.sendChat.resetHistory();
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
|
||||
party = await Group.findOne({ _id: party._id });
|
||||
party = await Group.findOne({ _id: party._id });
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
|
||||
info: {
|
||||
items: { blueFins: 0, fireCoral: 0 },
|
||||
quest: 'dilatoryDistress1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
|
||||
info: {
|
||||
items: { blueFins: 0, fireCoral: 0 },
|
||||
quest: 'dilatoryDistress1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles collection quests with multiple items', async () => {
|
||||
progress.collectedItems = 10;
|
||||
party.quest.key = 'evilsanta2';
|
||||
party.quest.active = false;
|
||||
it('handles correctly', async () => {
|
||||
progress.collectedItems = 10;
|
||||
party.quest.key = 'evilsanta2';
|
||||
party.quest.active = false;
|
||||
|
||||
await party.startQuest(questLeader);
|
||||
Group.prototype.sendChat.resetHistory();
|
||||
await party.save();
|
||||
await party.startQuest(questLeader);
|
||||
Group.prototype.sendChat.resetHistory();
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
|
||||
party = await Group.findOne({ _id: party._id });
|
||||
party = await Group.findOne({ _id: party._id });
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
|
||||
info: {
|
||||
quest: 'evilsanta2',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
|
||||
info: {
|
||||
quest: 'evilsanta2',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot collect excess items', async () => {
|
||||
// Make sure the quest progress isn't erased
|
||||
sandbox.stub(Group.prototype, 'finishQuest').returns(Promise.resolve());
|
||||
|
||||
progress.collectedItems = 500;
|
||||
party.quest.key = 'evilsanta2';
|
||||
party.quest.active = false;
|
||||
|
||||
await party.startQuest(questLeader);
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
party = await Group.findOne({ _id: party._id });
|
||||
|
||||
expect(party.quest.progress.collect.tracks)
|
||||
.to.eql(questScrolls.evilsanta2.collect.tracks.count);
|
||||
expect(party.quest.progress.collect.branches)
|
||||
.to.eql(questScrolls.evilsanta2.collect.branches.count);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2380,29 +2402,29 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('isSubscribed', () => {
|
||||
context('hasActiveGroupPlan', () => {
|
||||
it('returns false if group does not have customer id', () => {
|
||||
expect(party.isSubscribed()).to.be.undefined;
|
||||
expect(party.hasActiveGroupPlan()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns true if group does not have plan.dateTerminated', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(party.isSubscribed()).to.be.true;
|
||||
expect(party.hasActiveGroupPlan()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns true if group if plan.dateTerminated is after today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(party.isSubscribed()).to.be.true;
|
||||
expect(party.hasActiveGroupPlan()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if group if plan.dateTerminated is before today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(party.isSubscribed()).to.be.false;
|
||||
expect(party.hasActiveGroupPlan()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
TAVERN_ID,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
|
||||
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
|
||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
@@ -292,12 +290,6 @@ describe('POST /chat', () => {
|
||||
.that.includes(testBannedWords.join(', '));
|
||||
});
|
||||
|
||||
it('check all banned words are matched', async () => {
|
||||
const message = bannedWords.join(',').replace(/\\/g, '');
|
||||
const matches = getMatchesByWordArray(message, bannedWords);
|
||||
expect(matches.length).to.equal(bannedWords.length);
|
||||
});
|
||||
|
||||
it('does not error when bad word is suffix of a word', async () => {
|
||||
const wordAsSuffix = `prefix${testBannedWordMessage}`;
|
||||
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
|
||||
|
||||
@@ -11,7 +11,9 @@ import apiError from '../../../../../website/server/libs/apiError';
|
||||
|
||||
describe('GET /groups', () => {
|
||||
let user;
|
||||
let userInGuild;
|
||||
const NUMBER_OF_PUBLIC_GUILDS = 3; // 2 + the tavern
|
||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2;
|
||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
|
||||
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
|
||||
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
|
||||
@@ -33,14 +35,20 @@ describe('GET /groups', () => {
|
||||
name: 'public guild - is member',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
summary: 'ohayou kombonwa',
|
||||
description: 'oyasumi',
|
||||
});
|
||||
await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
|
||||
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
|
||||
|
||||
userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
|
||||
|
||||
publicGuildNotMember = await generateGroup(leader, {
|
||||
name: 'public guild - is not member',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
summary: 'Natsume Soseki',
|
||||
description: 'Kinnosuke no Hondana',
|
||||
categories,
|
||||
});
|
||||
|
||||
@@ -150,6 +158,35 @@ describe('GET /groups', () => {
|
||||
|
||||
expect(guilds.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('filters public guilds by leader role', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&leader=true');
|
||||
expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER);
|
||||
});
|
||||
|
||||
it('filters public guilds by member role', async () => {
|
||||
const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].name).to.have.string('is member');
|
||||
});
|
||||
|
||||
it('filters public guilds by single-word search term', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=kom');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
|
||||
});
|
||||
|
||||
it('filters public guilds by single-word search term left and right-padded by spaces', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
|
||||
});
|
||||
|
||||
it('filters public guilds by two-words search term separated by multiple spaces', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].description).to.have.string('Kinnosuke');
|
||||
});
|
||||
});
|
||||
|
||||
describe('public guilds pagination', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateChallenge,
|
||||
checkExistence,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import payments from '../../../../../website/server/libs/payments/payments';
|
||||
import { calculateSubscriptionTerminationDate } from '../../../../../website/server/libs/payments/util';
|
||||
|
||||
describe('POST /groups/:groupId/leave', () => {
|
||||
const typesOfGroups = {
|
||||
@@ -338,4 +340,48 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
each(typesOfGroups, (groupDetails, groupType) => {
|
||||
context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => {
|
||||
const extraMonths = 12;
|
||||
let groupWithPlan;
|
||||
let member;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails,
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
[member] = members;
|
||||
groupWithPlan = group;
|
||||
await member.update({
|
||||
'purchased.plan.extraMonths': extraMonths,
|
||||
});
|
||||
});
|
||||
it('calculates dateTerminated and sets extraMonths to zero after user leaves the group', async () => {
|
||||
const userBeforeLeave = await User.findById(member._id).exec();
|
||||
|
||||
await member.post(`/groups/${groupWithPlan._id}/leave`);
|
||||
const userAfterLeave = await User.findById(member._id).exec();
|
||||
|
||||
const dateTerminatedBefore = userBeforeLeave.purchased.plan.dateTerminated;
|
||||
const extraMonthsBefore = userBeforeLeave.purchased.plan.extraMonths;
|
||||
const dateTerminatedAfter = userAfterLeave.purchased.plan.dateTerminated;
|
||||
const extraMonthsAfter = userAfterLeave.purchased.plan.extraMonths;
|
||||
|
||||
const expectedTerminationDate = calculateSubscriptionTerminationDate(null, {
|
||||
customerId: payments.constants.GROUP_PLAN_CUSTOMER_ID,
|
||||
extraMonths,
|
||||
}, payments.constants);
|
||||
|
||||
expect(extraMonthsBefore).to.gte(12);
|
||||
expect(extraMonthsAfter).to.equal(0);
|
||||
expect(dateTerminatedBefore).to.be.null;
|
||||
expect(dateTerminatedAfter).to.exist;
|
||||
|
||||
expect(moment(dateTerminatedAfter).diff(expectedTerminationDate, 'days')).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,7 +112,7 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
|
||||
await Promise.all([partyMembers[0].sync(), questingGroup.sync()]);
|
||||
expect(leader.party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not begin the quest if pending invitations remain', async () => {
|
||||
|
||||
@@ -55,6 +55,18 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if reward value is a negative number', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with negative value',
|
||||
value: -10,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'reward validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update user.tasksOrder.{taskType} when the task is not saved because invalid', async () => {
|
||||
const originalHabitsOrder = (await user.get('/user')).tasksOrder.habits;
|
||||
await expect(user.post('/tasks/user', {
|
||||
|
||||
@@ -530,5 +530,15 @@ describe('PUT /tasks/:id', () => {
|
||||
|
||||
expect(savedReward.value).to.eql(100);
|
||||
});
|
||||
|
||||
it('returns an error if reward value is a negative number', async () => {
|
||||
await expect(user.put(`/tasks/${reward._id}`, {
|
||||
value: -10,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'reward validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -346,4 +346,23 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('user with Apple auth', async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
auth: {
|
||||
apple: {
|
||||
id: 'apple-id',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Apple user', async () => {
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
describe('POST /user/unlock', () => {
|
||||
let user;
|
||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||
const unlockCost = 1.25;
|
||||
const usersStartingGems = 5;
|
||||
|
||||
@@ -34,4 +35,25 @@ describe('POST /user/unlock', () => {
|
||||
expect(response.message).to.equal(t('unlocked'));
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
});
|
||||
|
||||
it('does not reduce a user\'s balance twice', async () => {
|
||||
await user.update({
|
||||
balance: usersStartingGems,
|
||||
});
|
||||
const response = await user.post(`/user/unlock?path=${unlockGearSetPath}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('unlocked'));
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
|
||||
expect(user.post(`/user/unlock?path=${unlockGearSetPath}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('alreadyUnlocked'),
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,4 +95,42 @@ describe('DELETE social registration', () => {
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
context('Apple', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/apple')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/apple');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.apple).to.be.undefined;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/apple');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import * as appleAuth from '../../../../../../website/server/libs/auth/apple';
|
||||
|
||||
describe('GET /user/auth/apple', () => {
|
||||
let api;
|
||||
let user;
|
||||
const appleEndpoint = '/user/auth/apple';
|
||||
|
||||
before(async () => {
|
||||
const expectedResult = { id: 'appleId', name: 'an apple user' };
|
||||
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.get(appleEndpoint);
|
||||
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -492,6 +492,74 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to google user', () => {
|
||||
let user;
|
||||
const email = 'some@email-google.net';
|
||||
const username = 'some-username-google';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to apple user', () => {
|
||||
let user;
|
||||
const email = 'some@email-apple.net';
|
||||
const username = 'some-username-apple';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('login is already taken', () => {
|
||||
let username; let email; let
|
||||
api;
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('POST /user/auth/social', () => {
|
||||
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
|
||||
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
@@ -106,6 +107,7 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,16 @@ describe('POST /user/webhook', () => {
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
});
|
||||
|
||||
it('ignores protected fields', async () => {
|
||||
body.failures = 3;
|
||||
body.lastFailureAt = new Date();
|
||||
|
||||
const webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.failures).to.eql(0);
|
||||
expect(webhook.lastFailureAt).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.webhooks).to.eql([]);
|
||||
|
||||
|
||||
@@ -63,6 +63,21 @@ describe('PUT /user/webhook/:id', () => {
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('ignores protected fields', async () => {
|
||||
const failures = 3;
|
||||
const lastFailureAt = new Date();
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {
|
||||
failures, lastFailureAt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
const webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.failures).to.eql(0);
|
||||
expect(webhook.lastFailureAt).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('updates a webhook with empty label', async () => {
|
||||
const url = 'http://a-new-url.com';
|
||||
const type = 'groupChatReceived';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
requester,
|
||||
translate,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('GET /faq', () => {
|
||||
describe('language parameter', () => {
|
||||
it('returns faq (and does not require authentication)', async () => {
|
||||
const res = await requester().get('/faq');
|
||||
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0].question).to.equal(translate('faqQuestion0'));
|
||||
});
|
||||
|
||||
it('returns faq not in English', async () => {
|
||||
const res = await requester().get('/faq?language=de');
|
||||
expect(res).to.have.nested.property('stillNeedHelp.ios');
|
||||
expect(res.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
|
||||
});
|
||||
|
||||
it('falls back to English if the desired language is not found', async () => {
|
||||
const res = await requester().get('/faq?language=wrong');
|
||||
expect(res).to.have.nested.property('stillNeedHelp.ios');
|
||||
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('platform parameter', () => {
|
||||
it('returns faq with answers for ios platform only', async () => {
|
||||
const res = await requester().get('/faq?platform=ios');
|
||||
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp).to.eql({ ios: translate('iosFaqStillNeedHelp') });
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
question: translate('faqQuestion0'),
|
||||
ios: translate('iosFaqAnswer0'),
|
||||
});
|
||||
});
|
||||
it('returns an error when invalid platform parameter is specified', async () => {
|
||||
const request = requester().get('/faq?platform=wrong');
|
||||
await expect(request)
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: i18n.t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
it('falls back to "web" description if there is no description for specified platform', async () => {
|
||||
const res = await requester().get('/faq?platform=android');
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp).to.eql({ web: translate('webFaqStillNeedHelp') });
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
question: translate('faqQuestion0'),
|
||||
android: translate('androidFaqAnswer0'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -457,6 +457,74 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to google user', () => {
|
||||
let user;
|
||||
const email = 'some-google@email.net';
|
||||
const username = 'some-username-google';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to apple user', () => {
|
||||
let user;
|
||||
const email = 'some-apple@email.net';
|
||||
const username = 'some-username-apple';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('login is already taken', () => {
|
||||
let username; let email; let
|
||||
api;
|
||||
|
||||
@@ -89,6 +89,18 @@ describe('shared.ops.feed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not allow feeding of wacky pets', done => {
|
||||
user.items.pets['Wolf-Veggie'] = 5;
|
||||
user.items.food.Meat = 1;
|
||||
try {
|
||||
feed(user, { params: { pet: 'Wolf-Veggie', food: 'Meat' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageCannotFeedPet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not allow feeding of mounts', done => {
|
||||
user.items.pets['Wolf-Base'] = -1;
|
||||
user.items.mounts['Wolf-Base'] = true;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import unlock from '../../../website/common/script/ops/unlock';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
||||
|
||||
describe('shared.ops.unlock', () => {
|
||||
let user;
|
||||
@@ -31,6 +26,15 @@ describe('shared.ops.unlock', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not unlock lost gear', done => {
|
||||
user.items.gear.owned.headAccessory_special_bearEars = false;
|
||||
|
||||
unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
|
||||
|
||||
expect(user.balance).to.equal(usersStartingGems);
|
||||
done();
|
||||
});
|
||||
|
||||
it('returns an error when user balance is too low', done => {
|
||||
user.balance = 0;
|
||||
|
||||
@@ -50,18 +54,30 @@ describe('shared.ops.unlock', () => {
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||
expect(user.balance).to.equal(3.75);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// disabled until fully implemente
|
||||
xit('returns an error when user already owns items in a full set', done => {
|
||||
it('returns an error when user already owns a full set of gear', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
unlock(user, { query: { path: unlockGearSetPath } });
|
||||
unlock(user, { query: { path: unlockGearSetPath } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||
expect(user.balance).to.equal(3.75);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
xit('returns an error when user already owns items in a full set', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: unlockPath.split(',').splice(2).join(',') } });
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlockedPart'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -78,7 +94,7 @@ describe('shared.ops.unlock', () => {
|
||||
expect(user.preferences.background).to.equal('giant_florals');
|
||||
});
|
||||
|
||||
it('un-equips an item already equipped', () => {
|
||||
it('un-equips a background already equipped', () => {
|
||||
expect(user.purchased.background.giant_florals).to.not.exist;
|
||||
|
||||
unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
||||
@@ -105,7 +121,7 @@ describe('shared.ops.unlock', () => {
|
||||
expect(user.items.gear.owned.headAccessory_special_wolfEars).to.be.true;
|
||||
});
|
||||
|
||||
it('unlocks a an item', () => {
|
||||
it('unlocks an item', () => {
|
||||
const [, message] = unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
|
||||
@@ -5,6 +5,8 @@ import { v4 as generateUUID } from 'uuid';
|
||||
import { ApiUser, ApiGroup, ApiChallenge } from '../api-classes';
|
||||
import { requester } from '../requester';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import payments from '../../../../website/server/libs/payments/payments';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
|
||||
// Creates a new user and returns it
|
||||
// If you need the user to have specific requirements,
|
||||
@@ -83,14 +85,35 @@ export async function generateGroup (leader, details = {}, update = {}) {
|
||||
return apiGroup;
|
||||
}
|
||||
|
||||
async function _upgradeToGroupPlan (groupLeader, group) {
|
||||
const groupLeaderModel = await User.findById(groupLeader._id).exec();
|
||||
|
||||
// Create subscription
|
||||
const paymentData = {
|
||||
user: groupLeaderModel,
|
||||
groupId: group._id,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
await payments.createSubscription(paymentData);
|
||||
}
|
||||
|
||||
// This is generate group + the ability to create
|
||||
// real users to populate it. The settings object
|
||||
// takes in:
|
||||
// members: Number - the number of group members to create.
|
||||
// Defaults to 0. Does not include group leader.
|
||||
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
|
||||
// invites: Number - the number of users to create and invite to the group. Defaults to 0.
|
||||
// groupDetails: Object - how to initialize the group
|
||||
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user
|
||||
// addGroupPlan: boolean - will add group plan with basic subscription. Defaults to false
|
||||
// can create the group
|
||||
//
|
||||
// Returns an object with
|
||||
@@ -101,6 +124,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
|
||||
export async function createAndPopulateGroup (settings = {}) {
|
||||
const numberOfMembers = settings.members || 0;
|
||||
const numberOfInvites = settings.invites || 0;
|
||||
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
|
||||
const { groupDetails } = settings;
|
||||
const leaderDetails = settings.leaderDetails || { balance: 10 };
|
||||
|
||||
@@ -130,6 +154,10 @@ export async function createAndPopulateGroup (settings = {}) {
|
||||
|
||||
await Promise.all(invitees.map(invitee => invitee.sync()));
|
||||
|
||||
if (upgradeToGroupPlan) {
|
||||
await _upgradeToGroupPlan(groupLeader, group);
|
||||
}
|
||||
|
||||
return {
|
||||
groupLeader,
|
||||
group,
|
||||
|
||||
@@ -13,38 +13,38 @@
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.2.3",
|
||||
"@vue/cli-plugin-eslint": "^4.2.3",
|
||||
"@vue/cli-plugin-router": "^4.2.3",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.2.3",
|
||||
"@vue/cli-service": "^4.2.3",
|
||||
"@storybook/addon-actions": "^5.3.14",
|
||||
"@storybook/addon-knobs": "^5.3.14",
|
||||
"@storybook/addon-links": "^5.3.14",
|
||||
"@storybook/addon-notes": "^5.3.14",
|
||||
"@storybook/vue": "^5.3.14",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-knobs": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
"@storybook/vue": "^5.3.18",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.9.0",
|
||||
"amplitude-js": "^5.11.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.6.1",
|
||||
"bootstrap-vue": "^2.12.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.4",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"habitica-markdown": "^1.4.0",
|
||||
"hellojs": "^1.18.4",
|
||||
"inspectpack": "^4.4.0",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.4.1",
|
||||
"jquery": "^3.5.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.2",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.15.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
@@ -58,9 +58,8 @@
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue2-perfect-scrollbar": "^1.3.0",
|
||||
"vuedraggable": "^2.23.1",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.42.0"
|
||||
"webpack": "^4.43.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
The files in the following subfolders:
|
||||
|
||||
- audio
|
||||
- emails
|
||||
- icons
|
||||
- merch
|
||||
- presskit
|
||||
|
||||
are not processed by Webpack so their filenames don't get hashed, but given that they almost never change, they're still cached for 1 week.
|
||||
|
||||
In case one of the files needs to be updated the filename should be changed if possible.
|
||||
|
||||
For more information see `website/server/middlewares/static.js`.
|
||||
@@ -20,10 +20,6 @@
|
||||
</svg>
|
||||
<!-- eslint-enable max-len -->
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<h2>{{ $t('tipTitle', {tipNumber: currentTipNumber}) }}</h2>
|
||||
<p>{{ currentTip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -33,7 +29,7 @@
|
||||
'resting': showRestingBanner
|
||||
}"
|
||||
>
|
||||
<banned-account-modal />
|
||||
<!-- <banned-account-modal /> -->
|
||||
<amazon-payments-modal v-if="!isStaticPage" />
|
||||
<payments-success-modal />
|
||||
<sub-cancel-modal-confirm v-if="isUserLoaded" />
|
||||
@@ -266,7 +262,6 @@ import {
|
||||
} from '@/libs/userlocalManager';
|
||||
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import bannedAccountModal from '@/components/bannedAccountModal';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
@@ -281,7 +276,6 @@ export default {
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
amazonPaymentsModal,
|
||||
bannedAccountModal,
|
||||
paymentsSuccessModal,
|
||||
subCancelModalConfirm,
|
||||
subCanceledModal,
|
||||
@@ -299,7 +293,6 @@ export default {
|
||||
audioSuffix: null,
|
||||
|
||||
loading: true,
|
||||
currentTipNumber: 0,
|
||||
bannerHidden: false,
|
||||
};
|
||||
},
|
||||
@@ -312,15 +305,6 @@ export default {
|
||||
castingSpell () {
|
||||
return this.$store.state.spellOptions.castingSpell;
|
||||
},
|
||||
currentTip () {
|
||||
const numberOfTips = 35 + 1;
|
||||
const min = 1;
|
||||
const randomNumber = Math.random() * (numberOfTips - min) + min;
|
||||
const tipNumber = Math.floor(randomNumber);
|
||||
this.currentTipNumber = tipNumber; // eslint-disable-line vue/no-side-effects-in-computed-properties, max-len
|
||||
|
||||
return this.$t(`tip${tipNumber}`);
|
||||
},
|
||||
showRestingBanner () {
|
||||
return !this.bannerHidden && this.user && this.user.preferences.sleep;
|
||||
},
|
||||
@@ -385,7 +369,8 @@ export default {
|
||||
return response;
|
||||
}, error => {
|
||||
if (error.response.status >= 400) {
|
||||
this.checkForBannedUser(error);
|
||||
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,
|
||||
// but their chat message still exists.
|
||||
@@ -403,7 +388,8 @@ export default {
|
||||
// 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) {
|
||||
this.$store.dispatch('auth:logout');
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Most server errors should return is click to dismiss errors, with some exceptions
|
||||
@@ -553,7 +539,7 @@ export default {
|
||||
|
||||
// Case where user is not logged in
|
||||
if (!parseSettings) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bannedMessage = this.$t('accountSuspended', {
|
||||
@@ -561,9 +547,10 @@ export default {
|
||||
userId: parseSettings.auth.apiId,
|
||||
});
|
||||
|
||||
if (errorMessage !== bannedMessage) return;
|
||||
if (errorMessage !== bannedMessage) return false;
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'banned-account');
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return true;
|
||||
},
|
||||
initializeModalStack () {
|
||||
// Manage modals
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
width: 68px;
|
||||
|
||||
@@ -1,60 +1,24 @@
|
||||
.promo_achievement_CottonCandyPink {
|
||||
.promo_armoire_backgrounds_202004 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px -316px;
|
||||
width: 204px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_armoire_backgrounds_202003 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -445px;
|
||||
background-position: -313px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_cosplay {
|
||||
.promo_mystery_202005 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 623px;
|
||||
height: 167px;
|
||||
}
|
||||
.promo_hugabug_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -445px;
|
||||
width: 420px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202003 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px -211px;
|
||||
background-position: 0px -223px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pi_day {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -593px;
|
||||
width: 273px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px -359px;
|
||||
background-position: -313px -148px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_dailies {
|
||||
.scene_casting_spells {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -168px;
|
||||
width: 327px;
|
||||
height: 276px;
|
||||
}
|
||||
.scene_gaining_achievement {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px 0px;
|
||||
width: 339px;
|
||||
height: 210px;
|
||||
}
|
||||
.scene_shanaqui {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px -168px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
background-position: 0px 0px;
|
||||
width: 312px;
|
||||
height: 222px;
|
||||
}
|
||||
|
||||
@@ -1,768 +1,396 @@
|
||||
.quest_TEMPLATE_FOR_MISSING_IMAGE {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -553px -1543px;
|
||||
background-position: -502px -1519px;
|
||||
width: 221px;
|
||||
height: 39px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dustbunnies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_egg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -362px;
|
||||
width: 165px;
|
||||
height: 207px;
|
||||
}
|
||||
.quest_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -1023px;
|
||||
width: 118px;
|
||||
height: 131px;
|
||||
}
|
||||
.quest_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_ferret {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_frog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px 0px;
|
||||
width: 221px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_ghost_stag {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1519px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_goldenknight3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px 0px;
|
||||
width: 219px;
|
||||
height: 231px;
|
||||
}
|
||||
.quest_gryphon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -1326px;
|
||||
background-position: -443px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_guineapig {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -446px;
|
||||
background-position: 0px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_harpy {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1323px 0px;
|
||||
background-position: -220px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_hedgehog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -437px -1326px;
|
||||
background-position: 0px -1332px;
|
||||
width: 219px;
|
||||
height: 186px;
|
||||
}
|
||||
.quest_hippo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -226px;
|
||||
background-position: -440px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_horse {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -226px;
|
||||
background-position: -660px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kangaroo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -226px;
|
||||
background-position: -880px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kraken {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1097px -1326px;
|
||||
background-position: -660px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_lostMasterclasser1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -663px -220px;
|
||||
background-position: -880px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -446px;
|
||||
background-position: -880px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -446px;
|
||||
background-position: 0px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -1249px;
|
||||
background-position: -1762px -570px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_mayhemMistiflying2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -446px;
|
||||
background-position: -220px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -883px 0px;
|
||||
background-position: -440px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_monkey {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -883px -220px;
|
||||
background-position: -660px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1308px -1106px;
|
||||
background-position: -1540px -214px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_moon2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -666px;
|
||||
background-position: -880px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -666px;
|
||||
background-position: -1100px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -666px;
|
||||
background-position: -1100px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -666px;
|
||||
background-position: -1100px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -666px;
|
||||
background-position: -1100px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_nudibranch {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1326px;
|
||||
background-position: -1540px -431px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_octopus {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -657px -1326px;
|
||||
background-position: -220px -1332px;
|
||||
width: 222px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_owl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1103px -440px;
|
||||
background-position: 0px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_peacock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1091px -1106px;
|
||||
background-position: -1540px -648px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_penguin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -1065px;
|
||||
background-position: -1762px -178px;
|
||||
width: 190px;
|
||||
height: 183px;
|
||||
}
|
||||
.quest_pterodactyl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -886px;
|
||||
background-position: -220px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rat {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -886px;
|
||||
background-position: -440px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_robot {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -886px;
|
||||
background-position: -660px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -657px -1106px;
|
||||
background-position: -1540px -865px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_rooster {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -712px;
|
||||
background-position: -1528px -1332px;
|
||||
width: 213px;
|
||||
height: 174px;
|
||||
}
|
||||
.quest_ruby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -223px 0px;
|
||||
background-position: -880px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sabretooth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1323px -220px;
|
||||
background-position: -1100px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_seaserpent {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1323px -440px;
|
||||
background-position: -1320px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sheep {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1323px -660px;
|
||||
background-position: -1320px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_silver {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1323px -880px;
|
||||
background-position: -1320px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_slime {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1106px;
|
||||
background-position: -1320px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sloth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -1106px;
|
||||
background-position: -1320px -880px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_snail {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -217px -1326px;
|
||||
background-position: -1320px -1112px;
|
||||
width: 219px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_snake {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px 0px;
|
||||
background-position: -877px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_spider {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1543px;
|
||||
background-position: -251px -1519px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_squirrel {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -886px;
|
||||
background-position: 0px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -402px -1543px;
|
||||
background-position: -1762px -721px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_stoikalmCalamity2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -886px;
|
||||
background-position: -220px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -886px;
|
||||
background-position: -440px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_taskwoodsTerror1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -251px -1543px;
|
||||
background-position: -1762px -872px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_taskwoodsTerror2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -874px -1106px;
|
||||
background-position: -1540px -1082px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_taskwoodsTerror3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1103px -660px;
|
||||
background-position: -660px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_treeling {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1314px -1326px;
|
||||
background-position: -1094px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_trex {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -887px;
|
||||
background-position: -1762px 0px;
|
||||
width: 204px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_trex_undead {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -534px;
|
||||
background-position: -1311px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_triceratops {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1103px -220px;
|
||||
background-position: -880px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_turtle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1103px 0px;
|
||||
background-position: -1100px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_unicorn {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -883px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_velociraptor {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px 0px;
|
||||
width: 222px;
|
||||
height: 225px;
|
||||
}
|
||||
.quest_vice1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -178px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_vice2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -663px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_vice3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -356px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_whale {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -443px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_yarn {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -1106px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_atom1_soapBars {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1356px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_dilatoryDistress1_blueFins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1408px;
|
||||
width: 51px;
|
||||
height: 48px;
|
||||
}
|
||||
.quest_dilatoryDistress1_fireCoral {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1509px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_egg_plainEgg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1304px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_evilsanta2_branches {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1694px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_evilsanta2_tracks {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1035px;
|
||||
width: 54px;
|
||||
height: 60px;
|
||||
}
|
||||
.quest_goldenknight1_testimony {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -196px -1694px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_lostMasterclasser1_ancientTome {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1726px -1583px;
|
||||
width: 33px;
|
||||
height: 42px;
|
||||
}
|
||||
.quest_lostMasterclasser1_forbiddenTome {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -285px -1694px;
|
||||
width: 33px;
|
||||
height: 42px;
|
||||
}
|
||||
.quest_lostMasterclasser1_hiddenTome {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -319px -1694px;
|
||||
width: 33px;
|
||||
height: 42px;
|
||||
}
|
||||
.quest_mayhemMistiflying2_mistifly1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -147px -1694px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_mayhemMistiflying2_mistifly2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -98px -1694px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_mayhemMistiflying2_mistifly3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -49px -1694px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_moon1_shard {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1694px -1353px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
.quest_moonstone1_moonstone {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -855px -1543px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.quest_robot_bolt {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1694px -1249px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_robot_gear {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1613px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_robot_spring {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1096px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_ruby_aquariusRune {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -594px -1652px;
|
||||
width: 39px;
|
||||
height: 40px;
|
||||
}
|
||||
.quest_ruby_rubyGem {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1457px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_ruby_venusRune {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -775px -1543px;
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
}
|
||||
.quest_silver_cancerRune {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -815px -1543px;
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
}
|
||||
.quest_silver_moonRune {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -245px -1694px;
|
||||
width: 39px;
|
||||
height: 42px;
|
||||
}
|
||||
.quest_silver_silverIngot {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1252px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_stoikalmCalamity2_icicleCoin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1200px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_taskwoodsTerror2_brownie {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1148px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_taskwoodsTerror2_dryad {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -1561px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_taskwoodsTerror2_pixie {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1694px -1301px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.quest_vice2_lightCrystal {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -553px -1652px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.inventory_quest_scroll_alligator {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -414px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_amber {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -483px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_armadillo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -552px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -690px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom1_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -621px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -828px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom2_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -759px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -966px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_atom3_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -897px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_axolotl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -345px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_badger {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -276px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_basilist {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -207px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_beetle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_bronze {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_bunny {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_butterfly {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1657px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_cheetah {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1588px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_cow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1519px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatoryDistress1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1381px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatoryDistress2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1243px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatoryDistress2_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1312px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatoryDistress3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1105px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatoryDistress3_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1174px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dilatory_derby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1450px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dolphin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1036px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dustbunnies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -967px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_egg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -898px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -829px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -760px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -691px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_ferret {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -622px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_frog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -553px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_ghost_stag {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1681px -1469px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_goldenknight1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1681px -1400px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_goldenknight1_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1612px -1469px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_goldenknight2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -1400px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_goldenknight2_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1612px -1400px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_goldenknight3_locked {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1543px -1469px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 26 KiB |