Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f988acb55 | |||
| eec57cb9b8 | |||
| f5eb868763 | |||
| d98932e183 | |||
| a91c2f7c7c | |||
| 70d057e66c | |||
| b680b6026b | |||
| 0c3b16ca74 | |||
| 7be039a35f | |||
| 01ae56f944 | |||
| 7b020e133f | |||
| ca413ff41a | |||
| 81eef79da4 | |||
| f1469b52f6 | |||
| ef118f23c2 | |||
| 964861bd6c | |||
| 32e9dbe1ed | |||
| 466fb4e42e | |||
| 8f923f7753 | |||
| 3bb8db45fd | |||
| 181317dbff | |||
| 73a7ef8b2c | |||
| 206e3468f4 | |||
| 77fde9d73f | |||
| c1d4bcbac3 | |||
| e98d0d88d9 | |||
| fc3a1dd93d | |||
| 897155e4c8 | |||
| 689f5ad634 | |||
| aa1ea74daa | |||
| 1b301e9c68 | |||
| c00b2247d4 | |||
| 88de5552a0 | |||
| 917c68d51f | |||
| 44e438303a | |||
| 7eeddcb033 | |||
| 2fb8e16e8f | |||
| 80ffcddb35 | |||
| f4c453675b | |||
| 5165d491b0 | |||
| 6559353613 | |||
| 9caacc8f6c | |||
| b21b5a4f4b | |||
| 21f3e704d4 | |||
| d0bc0dbe49 | |||
| 46b5efcaf6 | |||
| 633f3df372 | |||
| fa79fb6608 | |||
| f9d9df5ddb | |||
| 8dbbfcd3a1 | |||
| 9a07ba7417 | |||
| 8248c4ca4e | |||
| 0e9ac6d4f2 | |||
| 56df62cf49 | |||
| eb16966953 | |||
| 6255c5dcc7 | |||
| 60ffc1fdaf | |||
| 0cfe0473b9 | |||
| 10f89c8d79 | |||
| 0692eb10cc | |||
| 4ac160ab21 | |||
| dc5163e8a0 | |||
| d850b50009 | |||
| 65e00ef784 | |||
| fd11adbb82 | |||
| 4d64e299ef | |||
| 29c21f09e1 | |||
| 205436d5b1 | |||
| 2b8f94b244 | |||
| 30cedad9b2 | |||
| 3a67a36031 | |||
| 9d645c1c2e | |||
| 6ee4c9870a | |||
| fbfe35b4eb | |||
| 3b9509fa1a | |||
| 0f81c5cbdb | |||
| d3fdfe33fb | |||
| d9cf7d3f79 | |||
| ffe144e762 | |||
| cec3e67b16 | |||
| 2a94ff41b1 | |||
| ce32477af7 | |||
| 1b6b99b521 | |||
| b7964a411c | |||
| bce138f9c2 | |||
| d82213bfee | |||
| 535aa860f1 | |||
| 5d169d477a | |||
| d7ee1ec4f4 | |||
| 8c3a9c6dbc | |||
| 747c4ffbad | |||
| e30e2f23ac | |||
| d016c1fa0a | |||
| 5b2b51e4f6 | |||
| 7393ef5162 | |||
| 8a548a6a4d | |||
| 5ed007190a | |||
| 24afffc2ae | |||
| 2ff3c7326c | |||
| 8796dbd8b8 | |||
| d2fc7c0c3d | |||
| 13a5b276e9 | |||
| 0a47af4ac3 | |||
| c0bf2cffea | |||
| 954040dff8 | |||
| e7af07cebb | |||
| d7d7f82723 | |||
| 7daaf04d0d | |||
| dc8c40c613 | |||
| 9a0e029491 | |||
| d6cfbd5529 | |||
| 74244fd3ba | |||
| 65e15e0c1d | |||
| d21b9a5af4 | |||
| 7cedecf27e | |||
| 9aaeb2c4ac | |||
| df461f7642 | |||
| 0f72064923 | |||
| 139645ff76 | |||
| aedcb483a0 | |||
| cfb335ab78 | |||
| 70aea8c14a | |||
| f53022c00e | |||
| b82a79361b | |||
| ebb3a12e92 | |||
| 272a6ec19d | |||
| dbf2ee6d6d | |||
| af0b2ab16e | |||
| 43fb747c8f | |||
| 9291414f7b | |||
| e4c95275ac | |||
| bd3e783274 | |||
| 9089b64af3 | |||
| 59f4e7f598 | |||
| 1f8e2d5677 | |||
| c4049608a8 | |||
| 8003632041 | |||
| f4c840faec | |||
| e9bb171e04 | |||
| 13de119bbb | |||
| f27ece49d1 | |||
| fa98b724a9 | |||
| 093ef68f5c | |||
| a501b2a5a6 | |||
| 011d22330f | |||
| cfb4acad1a | |||
| a7bbdf1cd3 | |||
| 7546733ae9 | |||
| 9490159f64 | |||
| da5bb795ca | |||
| 26869e9006 | |||
| 11018156c5 | |||
| 7280c50963 | |||
| 35a6f4cb19 | |||
| 4b5500b13d | |||
| 8b3a5ce6fe | |||
| 6a53cd29bf | |||
| 79c64763ac | |||
| aaf32cc09b | |||
| 7ee6ff18ce | |||
| 234258b41e | |||
| c10b9b7993 | |||
| 0f945ee369 | |||
| 0c5bede1ed | |||
| d8ab69b3c7 | |||
| bea77e9520 | |||
| 108201a465 | |||
| 32e51bd551 | |||
| 6e03e41271 | |||
| 8d9851a489 | |||
| ad60946eeb | |||
| c5e02292c4 | |||
| e2d0ddfb39 | |||
| 3db2cd49a4 | |||
| 02cac78896 | |||
| fef9c74f9b | |||
| 858a8749a4 | |||
| fd7c5b3847 | |||
| 73aa32ca31 | |||
| ead0b6c56f | |||
| e550ca1531 | |||
| 88059f568c | |||
| 9f85d3927f | |||
| d8badb6d12 | |||
| f5e4e2150a | |||
| 6743dcb08a | |||
| e7c8833c9a | |||
| 4de5140cf7 | |||
| 7de5a51247 | |||
| f1173cee6a | |||
| 0261d12bd9 | |||
| e3bcc48481 | |||
| 8bbb0ddaee | |||
| 7a0733f5ac | |||
| 474bc6a2b6 | |||
| 0af5593611 | |||
| 1b190a594a | |||
| e54bd8f242 | |||
| 0cc7c4a078 | |||
| 254a5cf423 | |||
| 18bc8c3d63 | |||
| 15753de3a1 | |||
| c93bf3e498 | |||
| e89ff95a21 | |||
| a02c4c1cfd | |||
| 616a0b7509 | |||
| 2a0bc030d8 | |||
| 42b00ed381 | |||
| 928c88f2da | |||
| 768e71228c | |||
| 6082f77977 | |||
| f6717a0bc1 | |||
| 9b3f8981e5 | |||
| 2758414b54 | |||
| e49aabdddf | |||
| aa371de8ae | |||
| 0acc7d19c5 | |||
| d861236f44 | |||
| 195928e471 | |||
| c27d52098b | |||
| 9e82ba261b | |||
| 566dd2b6b1 | |||
| 1a769d4a45 | |||
| f3fe1d76ad | |||
| 955c41f744 | |||
| 00c9fabf8b | |||
| 82d1df67c9 | |||
| 9449e6a883 | |||
| fa72684c53 | |||
| 45a22470fa | |||
| a388abc124 | |||
| 453d60b5bf | |||
| af1d13d3a2 | |||
| 680e86d2c9 | |||
| cc5e3ed123 | |||
| 4a6c0168a9 | |||
| 9d29f065e9 |
@@ -22,6 +22,7 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run lint-no-fix
|
||||
apidoc:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -42,6 +43,7 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run apidoc
|
||||
sanity:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -62,6 +64,7 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:sanity
|
||||
|
||||
common:
|
||||
@@ -83,6 +86,7 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:common
|
||||
content:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -103,6 +107,7 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:content
|
||||
|
||||
api-unit:
|
||||
@@ -110,6 +115,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -118,13 +124,18 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo docker run --name mongo -d -p 27017:27017 mongo:4.2
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:api:unit
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
@@ -133,6 +144,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -141,13 +153,18 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo docker run --name mongo -d -p 27017:27017 mongo:4.2
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:api-v3:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
@@ -156,6 +173,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -164,13 +182,18 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo docker run --name mongo -d -p 27017:27017 mongo:4.2
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:api-v4:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
@@ -194,5 +217,6 @@ jobs:
|
||||
npm ci
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:unit
|
||||
working-directory: ./website/client
|
||||
@@ -38,7 +38,12 @@ yarn.lock
|
||||
.elasticbeanstalk/*
|
||||
!.elasticbeanstalk/*.cfg.yml
|
||||
!.elasticbeanstalk/*.global.yml
|
||||
|
||||
/.vscode
|
||||
|
||||
# webstorm fake webpack for path intellisense
|
||||
webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitrpg",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
@@ -70,7 +71,6 @@
|
||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitrpg_test",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
@@ -80,5 +80,9 @@
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"BLOCKED_IPS": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false"
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PORT": "1234",
|
||||
"REDIS_PASSWORD": "12345678"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import gulp from 'gulp';
|
||||
import path from 'path';
|
||||
import babel from 'gulp-babel';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import clean from 'rimraf';
|
||||
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
.pipe(babel())
|
||||
@@ -24,10 +31,67 @@ gulp.task('build:prod', gulp.series(
|
||||
done => done(),
|
||||
));
|
||||
|
||||
// Due to this issue https://github.com/vkarpov15/run-rs/issues/45
|
||||
// When used on windows `run-rs` must first be run without the `--keep` option
|
||||
// in order to be setup correctly, afterwards it can be used.
|
||||
|
||||
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
|
||||
|
||||
gulp.task('build:prepare-mongo', async () => {
|
||||
if (fs.existsSync(MONGO_PATH)) {
|
||||
// console.log('MongoDB data folder exists, skipping setup.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (os.platform() !== 'win32') {
|
||||
// console.log('Not on Windows, skipping MongoDB setup.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
|
||||
|
||||
// use run-rs without --keep, kill it as soon as the replica set starts
|
||||
const runRsProcess = spawn('run-rs', ['-v', '4.2.8', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
|
||||
|
||||
for await (const chunk of runRsProcess.stdout) {
|
||||
const stringChunk = chunk.toString();
|
||||
console.log(stringChunk); // eslint-disable-line no-console
|
||||
// kills the process after the replica set is setup
|
||||
if (stringChunk.includes('Started replica set')) {
|
||||
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
|
||||
runRsProcess.kill();
|
||||
}
|
||||
}
|
||||
|
||||
let error = '';
|
||||
for await (const chunk of runRsProcess.stderr) {
|
||||
const stringChunk = chunk.toString();
|
||||
error += stringChunk;
|
||||
}
|
||||
|
||||
const exitCode = await new Promise(resolve => {
|
||||
runRsProcess.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode || error.length > 0) {
|
||||
// remove any leftover files
|
||||
clean.sync(MONGO_PATH);
|
||||
|
||||
throw new Error(`Error running run-rs: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('build:dev', gulp.series(
|
||||
'build:prepare-mongo',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
const buildArgs = [];
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
buildArgs.push('build:prod');
|
||||
} else if (process.env.NODE_ENV !== 'test') { // eslint-disable-line no-process-env
|
||||
buildArgs.push('build:dev');
|
||||
}
|
||||
|
||||
gulp.task('build', gulp.series(buildArgs, done => {
|
||||
|
||||
@@ -3,6 +3,10 @@ import nconf from 'nconf';
|
||||
import repl from 'repl';
|
||||
import gulp from 'gulp';
|
||||
import logger from '../website/server/libs/logger';
|
||||
import {
|
||||
getDevelopmentConnectionUrl,
|
||||
getDefaultConnectionOptions,
|
||||
} from '../website/server/libs/mongodb';
|
||||
|
||||
// Add additional properties to the repl's context
|
||||
const improveRepl = context => {
|
||||
@@ -26,13 +30,14 @@ const improveRepl = context => {
|
||||
context.Group = require('../website/server/models/group').model; // eslint-disable-line global-require
|
||||
context.User = require('../website/server/models/user').model; // eslint-disable-line global-require
|
||||
|
||||
const isProd = nconf.get('NODE_ENV') === 'production';
|
||||
const mongooseOptions = !isProd ? {} : {
|
||||
keepAlive: 1,
|
||||
connectTimeoutMS: 30000,
|
||||
};
|
||||
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
||||
const NODE_DB_URI = nconf.get('NODE_DB_URI');
|
||||
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
const connectionUrl = IS_PROD ? NODE_DB_URI : getDevelopmentConnectionUrl(NODE_DB_URI);
|
||||
|
||||
mongoose.connect(
|
||||
nconf.get('NODE_DB_URI'),
|
||||
connectionUrl,
|
||||
mongooseOptions,
|
||||
err => {
|
||||
if (err) throw err;
|
||||
|
||||
@@ -6,6 +6,10 @@ import nconf from 'nconf';
|
||||
import {
|
||||
pipe,
|
||||
} from './taskHelper';
|
||||
import {
|
||||
getDevelopmentConnectionUrl,
|
||||
getDefaultConnectionOptions,
|
||||
} from '../website/server/libs/mongodb';
|
||||
|
||||
// TODO rewrite
|
||||
|
||||
@@ -44,7 +48,10 @@ gulp.task('test:nodemon', gulp.series(done => {
|
||||
}, 'nodemon'));
|
||||
|
||||
gulp.task('test:prepare:mongo', cb => {
|
||||
mongoose.connect(TEST_DB_URI, err => {
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||
|
||||
mongoose.connect(connectionUrl, mongooseOptions, err => {
|
||||
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
|
||||
return mongoose.connection.dropDatabase(err2 => {
|
||||
if (err2) return cb(err2);
|
||||
@@ -176,7 +183,7 @@ gulp.task('test:api:unit:run', done => {
|
||||
|
||||
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
|
||||
|
||||
gulp.task('test:api-v3:integration', done => {
|
||||
gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo', done => {
|
||||
const runner = exec(
|
||||
testBin('istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
@@ -189,7 +196,7 @@ gulp.task('test:api-v3:integration', done => {
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:api-v3:integration:watch', () => gulp.watch([
|
||||
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
|
||||
@@ -206,7 +213,7 @@ gulp.task('test:api-v3:integration:separate-server', done => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:api-v4:integration', done => {
|
||||
gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo', done => {
|
||||
const runner = exec(
|
||||
testBin('istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
@@ -219,7 +226,7 @@ gulp.task('test:api-v4:integration', done => {
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:api-v4:integration:separate-server', done => {
|
||||
const runner = exec(
|
||||
@@ -231,11 +238,16 @@ gulp.task('test:api-v4:integration:separate-server', done => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:api:unit', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
'test:api:unit:run',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
gulp.task('test', gulp.series(
|
||||
'test:sanity',
|
||||
'test:content',
|
||||
'test:common',
|
||||
'test:prepare:mongo',
|
||||
'test:api:unit:run',
|
||||
'test:api-v3:integration',
|
||||
'test:api-v4:integration',
|
||||
@@ -243,14 +255,7 @@ gulp.task('test', gulp.series(
|
||||
));
|
||||
|
||||
gulp.task('test:api-v3', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
'test:api:unit:run',
|
||||
'test:api:unit',
|
||||
'test:api-v3:integration',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
gulp.task('test:api:unit', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
'test:api:unit:run',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20200721_summer_splash_orcas';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set;
|
||||
|
||||
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME };
|
||||
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.pets.Orca-Base': 5 };
|
||||
} else {
|
||||
set = { migration: MIGRATION_NAME, 'items.mounts.Orca-Base': true };
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-06-21')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20200731_naming_day';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set;
|
||||
let push;
|
||||
const inc = {
|
||||
'items.food.Cake_Base': 1,
|
||||
'items.food.Cake_CottonCandyBlue': 1,
|
||||
'items.food.Cake_CottonCandyPink': 1,
|
||||
'items.food.Cake_Desert': 1,
|
||||
'items.food.Cake_Golden': 1,
|
||||
'items.food.Cake_Red': 1,
|
||||
'items.food.Cake_Shade': 1,
|
||||
'items.food.Cake_Skeleton': 1,
|
||||
'items.food.Cake_White': 1,
|
||||
'items.food.Cake_Zombie': 1,
|
||||
'achievements.habiticaDays': 1,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME };
|
||||
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': false };
|
||||
push = { pinnedItems: { type: 'marketGear', path: 'gear.flat.back_special_namingDay2020', _id: uuid() }};
|
||||
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': false };
|
||||
push = { pinnedItems: { type: 'marketGear', path: 'gear.flat.body_special_namingDay2018', _id: uuid() }};
|
||||
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': false };
|
||||
push = { pinnedItems: { type: 'marketGear', path: 'gear.flat.head_special_namingDay2017', _id: uuid() }};
|
||||
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
|
||||
} else {
|
||||
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (push) {
|
||||
return await User.update({ _id: user._id }, { $set: set, $inc: inc, $push: push }).exec();
|
||||
} else {
|
||||
return await User.update({ _id: user._id }, { $set: set, $inc: inc }).exec();
|
||||
}
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-07-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20200218_pet_color_achievements';
|
||||
const MIGRATION_NAME = '20200818_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
@@ -14,31 +14,31 @@ async function updateUser (user) {
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-CottonCandyPink'] > 0
|
||||
&& pets['TigerCub-CottonCandyPink'] > 0
|
||||
&& pets['PandaCub-CottonCandyPink'] > 0
|
||||
&& pets['LionCub-CottonCandyPink'] > 0
|
||||
&& pets['Fox-CottonCandyPink'] > 0
|
||||
&& pets['FlyingPig-CottonCandyPink'] > 0
|
||||
&& pets['Dragon-CottonCandyPink'] > 0
|
||||
&& pets['Cactus-CottonCandyPink'] > 0
|
||||
&& pets['BearCub-CottonCandyPink'] > 0) {
|
||||
set['achievements.tickledPink'] = true;
|
||||
if (pets['Wolf-Golden'] > 0
|
||||
&& pets['TigerCub-Golden'] > 0
|
||||
&& pets['PandaCub-Golden'] > 0
|
||||
&& pets['LionCub-Golden'] > 0
|
||||
&& pets['Fox-Golden'] > 0
|
||||
&& pets['FlyingPig-Golden'] > 0
|
||||
&& pets['Dragon-Golden'] > 0
|
||||
&& pets['Cactus-Golden'] > 0
|
||||
&& pets['BearCub-Golden'] > 0) {
|
||||
set['achievements.goodAsGold'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-CottonCandyPink']
|
||||
&& mounts['TigerCub-CottonCandyPink']
|
||||
&& mounts['PandaCub-CottonCandyPink']
|
||||
&& mounts['LionCub-CottonCandyPink']
|
||||
&& mounts['Fox-CottonCandyPink']
|
||||
&& mounts['FlyingPig-CottonCandyPink']
|
||||
&& mounts['Dragon-CottonCandyPink']
|
||||
&& mounts['Cactus-CottonCandyPink']
|
||||
&& mounts['BearCub-CottonCandyPink'] ) {
|
||||
set['achievements.rosyOutlook'] = true;
|
||||
if (mounts['Wolf-Golden']
|
||||
&& mounts['TigerCub-Golden']
|
||||
&& mounts['PandaCub-Golden']
|
||||
&& mounts['LionCub-Golden']
|
||||
&& mounts['Fox-Golden']
|
||||
&& mounts['FlyingPig-Golden']
|
||||
&& mounts['Dragon-Golden']
|
||||
&& mounts['Cactus-Golden']
|
||||
&& mounts['BearCub-Golden'] ) {
|
||||
set['achievements.allThatGlitters'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ async function updateUser (user) {
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-02-01') },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-08-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.148.0",
|
||||
"version": "4.156.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.10.3",
|
||||
"@babel/preset-env": "^7.10.3",
|
||||
"@babel/core": "^7.11.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/register": "^7.10.3",
|
||||
"@google-cloud/trace-agent": "^5.1.0",
|
||||
"@slack/client": "^4.12.0",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.8",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.23.0",
|
||||
"apidoc": "^0.25.0",
|
||||
"apn": "^2.2.0",
|
||||
"apple-auth": "^1.0.6",
|
||||
"bcrypt": "^5.0.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.4.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.5.0",
|
||||
"csv-stringify": "^5.5.1",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -30,7 +30,7 @@
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.1.6",
|
||||
"got": "^11.3.0",
|
||||
"got": "^11.5.2",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
@@ -42,37 +42,39 @@
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.8.1",
|
||||
"lodash": "^4.17.15",
|
||||
"jwks-rsa": "^1.9.0",
|
||||
"lodash": "^4.17.20",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.27.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.20",
|
||||
"mongoose": "^5.10.2",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
"node-gcm": "^1.0.3",
|
||||
"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",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"redis": "^3.0.2",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
"stripe": "^7.15.0",
|
||||
"superagent": "^5.3.1",
|
||||
"universal-analytics": "^0.4.17",
|
||||
"superagent": "^6.1.0",
|
||||
"universal-analytics": "^0.4.23",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.2.0",
|
||||
"uuid": "^8.3.0",
|
||||
"validator": "^13.1.1",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.3.3",
|
||||
"winston-loggly-bulk": "^3.1.0",
|
||||
"winston-loggly-bulk": "^3.1.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"private": true,
|
||||
@@ -102,6 +104,7 @@
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 4.2.8 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"postinstall": "gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
@@ -109,13 +112,16 @@
|
||||
"axios": "^0.19.2",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.0",
|
||||
"monk": "^7.3.1",
|
||||
"require-again": "^2.0.0",
|
||||
"sinon": "^9.0.2",
|
||||
"run-rs": "^0.6.2",
|
||||
"sinon": "^9.0.3",
|
||||
"sinon-chai": "^3.5.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -31,19 +31,22 @@ async function deleteAmplitudeData (userId, email) {
|
||||
console.log(`${userId} (${email}) Amplitude response: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async function deleteHabiticaData (user, email) {
|
||||
const truncatedEmail = email.slice(0, email.indexOf('@'));
|
||||
const set = {
|
||||
'auth.blocked': false,
|
||||
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
|
||||
'auth.local.passwordHashMethod': 'bcrypt',
|
||||
};
|
||||
if (!user.auth.local.email) set['auth.local.email'] = `${truncatedEmail}@example.com`;
|
||||
if (!user.auth.local.email) set['auth.local.email'] = `${truncatedEmail}-gdpr@example.com`;
|
||||
await User.update(
|
||||
{ _id: user._id },
|
||||
{ $set: set },
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const response = await axios.delete(
|
||||
`${BASE_URL}/api/v3/user`,
|
||||
{
|
||||
|
||||
@@ -42,13 +42,13 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||
const timezoneOffsetFromUserPrefs = 1;
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets user.items.lastDrop.count', () => {
|
||||
@@ -240,7 +240,7 @@ describe('cron', () => {
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
|
||||
it('does not increment consecutive benefits after the first month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -256,7 +256,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the second month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -272,7 +272,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the third month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -288,7 +288,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the fourth month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -304,7 +304,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -339,7 +339,7 @@ describe('cron', () => {
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -352,7 +352,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -365,7 +365,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -378,7 +378,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -391,7 +391,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -404,7 +404,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -417,7 +417,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -430,7 +430,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -465,7 +465,7 @@ describe('cron', () => {
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -478,7 +478,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -491,7 +491,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -504,7 +504,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -517,7 +517,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -552,7 +552,7 @@ describe('cron', () => {
|
||||
user12.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -565,7 +565,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -578,7 +578,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -591,7 +591,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -604,7 +604,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -641,7 +641,7 @@ describe('cron', () => {
|
||||
user3g.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -654,7 +654,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -667,7 +667,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -680,7 +680,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -717,7 +717,7 @@ describe('cron', () => {
|
||||
user6x.purchased.plan.consecutive.gemCapExtra = 15;
|
||||
|
||||
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -730,7 +730,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -743,7 +743,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -756,7 +756,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
|
||||
describe('mongodb', () => {
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('getDevelopmentConnectionUrl', () => {
|
||||
it('returns the original connection url if not on windows', () => {
|
||||
sandbox.stub(os, 'platform').returns('linux');
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const originalString = 'mongodb://localhost:3030';
|
||||
const string = mongoLibOverride.getDevelopmentConnectionUrl(originalString);
|
||||
expect(string).to.equal(originalString);
|
||||
});
|
||||
|
||||
it('replaces localhost with hostname on windows', () => {
|
||||
sandbox.stub(os, 'platform').returns('win32');
|
||||
sandbox.stub(os, 'hostname').returns('hostname');
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const originalString = 'mongodb://localhost:3030';
|
||||
const string = mongoLibOverride.getDevelopmentConnectionUrl(originalString);
|
||||
expect(string).to.equal('mongodb://hostname:3030');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionOptions', () => {
|
||||
it('returns development config when IS_PROD is false', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
|
||||
it('returns production config when IS_PROD is true', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology', 'keepAlive', 'keepAliveInitialDelay']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,11 +31,14 @@ describe('#upgradeGroupPlan', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.resolves([]);
|
||||
|
||||
|
||||
@@ -21,11 +21,14 @@ describe('Canceling a subscription for group', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
@@ -141,6 +144,8 @@ describe('Canceling a subscription for group', () => {
|
||||
|
||||
it('prevents non group leader from managing subscription', async () => {
|
||||
const groupMember = new User();
|
||||
groupMember.guilds.push(group._id);
|
||||
await groupMember.save();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -162,7 +167,9 @@ describe('Canceling a subscription for group', () => {
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
const newLeader = new User();
|
||||
newLeader.profile.name = 'newLeader';
|
||||
updatedGroup.leader = newLeader._id;
|
||||
await newLeader.save();
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
@@ -185,8 +192,6 @@ describe('Canceling a subscription for group', () => {
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
await api.createSubscription(data);
|
||||
@@ -211,10 +216,15 @@ describe('Canceling a subscription for group', () => {
|
||||
await api.createSubscription(data);
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.have.callCount(4);
|
||||
expect(sender.sendTxn.thirdCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.thirdCall.args[1]).to.equal('group-member-cancel');
|
||||
expect(sender.sendTxn.thirdCall.args[2]).to.eql([
|
||||
expect(sender.sendTxn).to.be.have.callCount(6);
|
||||
const recipientCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isRecipient = call.args[0]._id === recipient._id;
|
||||
const isGroupMemberCancel = call.args[1] === 'group-member-cancel';
|
||||
return isRecipient && isGroupMemberCancel;
|
||||
});
|
||||
expect(recipientCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(recipientCall.args[1]).to.equal('group-member-cancel');
|
||||
expect(recipientCall.args[2]).to.eql([
|
||||
{ name: 'LEADER', content: user.profile.name },
|
||||
{ name: 'GROUP_NAME', content: group.name },
|
||||
]);
|
||||
@@ -246,8 +256,6 @@ describe('Canceling a subscription for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -259,11 +267,13 @@ describe('Canceling a subscription for group', () => {
|
||||
const group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
user.guilds.push(group2._id);
|
||||
await user.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
@@ -285,8 +295,6 @@ describe('Canceling a subscription for group', () => {
|
||||
});
|
||||
|
||||
it('does cancel a leader subscription with two cancelled group plans', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -298,7 +306,7 @@ describe('Canceling a subscription for group', () => {
|
||||
const group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
user.guilds.push(group2._id);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import i18n from '../../../../../../website/common/script/i18n';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
|
||||
@@ -33,11 +34,14 @@ describe('Purchasing a group plan for group', () => {
|
||||
group = generateGroup({
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
@@ -112,6 +116,30 @@ describe('Purchasing a group plan for group', () => {
|
||||
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not create a group plan for a public guild', async () => {
|
||||
const publicGroup = generateGroup({
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await publicGroup.save();
|
||||
|
||||
expect(publicGroup.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = publicGroup._id;
|
||||
|
||||
await expect(api.createSubscription(data))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyPrivateGuildsCanUpgrade'),
|
||||
});
|
||||
|
||||
const updatedGroup = await Group.findById(publicGroup._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.planId).to.not.exist;
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
@@ -148,8 +176,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
});
|
||||
|
||||
it('grants all members of a group a subscription', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -179,17 +205,28 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
expect(sender.sendTxn).to.be.calledThrice;
|
||||
const recipientCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isRecipient = call.args[0]._id === recipient._id;
|
||||
const isJoin = call.args[1] === 'group-member-join';
|
||||
return isRecipient && isJoin;
|
||||
});
|
||||
expect(recipientCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(recipientCall.args[1]).to.equal('group-member-join');
|
||||
expect(recipientCall.args[2]).to.eql([
|
||||
{ name: 'LEADER', content: user.profile.name },
|
||||
{ name: 'GROUP_NAME', content: group.name },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE },
|
||||
]);
|
||||
|
||||
// confirm that the other email sent is appropriate:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
const leaderCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isLeader = call.args[0]._id === group.leader;
|
||||
const isSubscriptionBegin = call.args[1] === 'group-subscription-begins';
|
||||
return isLeader && isSubscriptionBegin;
|
||||
});
|
||||
expect(leaderCall.args[0]._id).to.equal(group.leader);
|
||||
expect(leaderCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
|
||||
@@ -205,17 +242,28 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
expect(sender.sendTxn).to.be.calledThrice;
|
||||
const recipientCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isRecipient = call.args[0]._id === recipient._id;
|
||||
const isJoin = call.args[1] === 'group-member-join';
|
||||
return isRecipient && isJoin;
|
||||
});
|
||||
expect(recipientCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(recipientCall.args[1]).to.equal('group-member-join');
|
||||
expect(recipientCall.args[2]).to.eql([
|
||||
{ name: 'LEADER', content: user.profile.name },
|
||||
{ name: 'GROUP_NAME', content: group.name },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL },
|
||||
]);
|
||||
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
const leaderCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isLeader = call.args[0]._id === group.leader;
|
||||
const isSubscriptionBegin = call.args[1] === 'group-subscription-begins';
|
||||
return isLeader && isSubscriptionBegin;
|
||||
});
|
||||
expect(leaderCall.args[0]._id).to.equal(group.leader);
|
||||
expect(leaderCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
|
||||
@@ -238,17 +286,28 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
expect(sender.sendTxn).to.be.calledThrice;
|
||||
const recipientCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isRecipient = call.args[0]._id === recipient._id;
|
||||
const isJoin = call.args[1] === 'group-member-join';
|
||||
return isRecipient && isJoin;
|
||||
});
|
||||
expect(recipientCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(recipientCall.args[1]).to.equal('group-member-join');
|
||||
expect(recipientCall.args[2]).to.eql([
|
||||
{ name: 'LEADER', content: user.profile.name },
|
||||
{ name: 'GROUP_NAME', content: group.name },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL },
|
||||
]);
|
||||
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
const leaderCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isLeader = call.args[0]._id === group.leader;
|
||||
const isSubscriptionBegin = call.args[1] === 'group-subscription-begins';
|
||||
return isLeader && isSubscriptionBegin;
|
||||
});
|
||||
expect(leaderCall.args[0]._id).to.equal(group.leader);
|
||||
expect(leaderCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
});
|
||||
@@ -275,17 +334,28 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
expect(sender.sendTxn).to.be.calledThrice;
|
||||
const recipientCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isRecipient = call.args[0]._id === recipient._id;
|
||||
const isJoin = call.args[1] === 'group-member-join';
|
||||
return isRecipient && isJoin;
|
||||
});
|
||||
expect(recipientCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(recipientCall.args[1]).to.equal('group-member-join');
|
||||
expect(recipientCall.args[2]).to.eql([
|
||||
{ name: 'LEADER', content: user.profile.name },
|
||||
{ name: 'GROUP_NAME', content: group.name },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL },
|
||||
]);
|
||||
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
const leaderCall = sender.sendTxn.getCalls().find(call => {
|
||||
const isLeader = call.args[0]._id === group.leader;
|
||||
const isSubscriptionBegin = call.args[1] === 'group-subscription-begins';
|
||||
return isLeader && isSubscriptionBegin;
|
||||
});
|
||||
expect(leaderCall.args[0]._id).to.equal(group.leader);
|
||||
expect(leaderCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
@@ -302,8 +372,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -356,8 +424,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -419,8 +485,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -455,8 +519,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -483,8 +545,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -511,8 +571,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -541,8 +599,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -566,8 +622,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
@@ -589,8 +643,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
@@ -611,8 +663,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -632,8 +682,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -653,8 +701,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -688,8 +734,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -713,8 +757,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -726,13 +768,15 @@ describe('Purchasing a group plan for group', () => {
|
||||
const group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
user.guilds.push(group2._id);
|
||||
await user.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -757,8 +801,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -770,17 +812,22 @@ describe('Purchasing a group plan for group', () => {
|
||||
const group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
user.guilds.push(group2._id);
|
||||
await user.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
const updatedGroup = await Group.findById(group._id).exec();
|
||||
updatedGroup.memberCount = 2;
|
||||
await updatedGroup.save();
|
||||
|
||||
await updatedGroup.leave(recipient);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
@@ -806,8 +853,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -835,8 +880,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -864,8 +907,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -894,8 +935,6 @@ describe('Purchasing a group plan for group', () => {
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -33,11 +33,14 @@ describe('Stripe - Upgrade Group Plan', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.resolves([]);
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('preenHistory', () => {
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
now: Number(moment('2013-10-20').utcOffset(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,8 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
'Access-Control-Allow-Headers': 'Authorization,Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
'Access-Control-Expose-Headers': 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After',
|
||||
});
|
||||
expect(res.sendStatus).to.not.have.been.called;
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -33,7 +34,8 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
'Access-Control-Allow-Headers': 'Authorization,Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
'Access-Control-Expose-Headers': 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After',
|
||||
});
|
||||
expect(res.sendStatus).to.have.been.calledWith(200);
|
||||
expect(next).to.not.have.been.called;
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('ipBlocker middleware', () => {
|
||||
});
|
||||
|
||||
it('does not throw when the ip does not match', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
@@ -65,30 +65,12 @@ describe('ipBlocker middleware', () => {
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('throws when a matching ip exist in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
it('throws when the ip is blocked', () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import nconf from 'nconf';
|
||||
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { TooManyRequests } from '../../../../website/server/libs/errors';
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
|
||||
describe('rateLimiter middleware', () => {
|
||||
const pathToRateLimiter = '../../../../website/server/middlewares/rateLimiter';
|
||||
|
||||
let res; let req; let next; let nconfGetStub;
|
||||
|
||||
beforeEach(() => {
|
||||
nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
|
||||
nconfGetStub.withArgs('NODE_ENV').returns('test');
|
||||
nconfGetStub.withArgs('IS_TEST').returns(true);
|
||||
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
expect(res.set).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('is disabled when the env var is an not "true"', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
expect(res.set).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
|
||||
expect(res.set).to.have.been.calledOnce;
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 29,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
sandbox.stub(logger, 'error');
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
expect(res.set).to.not.have.been.called;
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
expect(logger.error).to.have.been.calledWithMatch(Error, 'Rate Limiter Error');
|
||||
});
|
||||
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
// call for 31 times
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
await attachRateLimiter(req, res, next); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
expect(next).to.have.been.callCount(31);
|
||||
const calledWith = next.getCall(30).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('clientRateLimited'));
|
||||
expect(calledWith[0] instanceof TooManyRequests).to.equal(true);
|
||||
|
||||
expect(res.set).to.have.been.callCount(31);
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'Retry-After': sinon.match(Number),
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 0,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
req.headers['x-api-user'] = 'user-1';
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
// user id an ip are counted as separate sources
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 28, // 2 calls with user id
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = undefined;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 27, // 3 calls with only ip
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@ describe('redirects middleware', () => {
|
||||
const nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front';
|
||||
|
||||
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||
@@ -37,7 +37,7 @@ describe('redirects middleware', () => {
|
||||
const nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('https');
|
||||
req.protocol = 'https';
|
||||
req.originalUrl = '/static/front';
|
||||
|
||||
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||
@@ -51,7 +51,7 @@ describe('redirects middleware', () => {
|
||||
const nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||
nconfStub.withArgs('IS_PROD').returns(false);
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front';
|
||||
|
||||
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||
@@ -65,7 +65,7 @@ describe('redirects middleware', () => {
|
||||
const nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('http://habitica.com');
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front';
|
||||
|
||||
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||
@@ -81,7 +81,7 @@ describe('redirects middleware', () => {
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
|
||||
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front';
|
||||
req.query.skipSSLCheck = 'test-key';
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('redirects middleware', () => {
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
|
||||
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front?skipSSLCheck=INVALID';
|
||||
req.query.skipSSLCheck = 'INVALID';
|
||||
|
||||
@@ -114,7 +114,7 @@ describe('redirects middleware', () => {
|
||||
nconfStub.withArgs('IS_PROD').returns(true);
|
||||
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns(null);
|
||||
|
||||
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||
req.protocol = 'http';
|
||||
req.originalUrl = '/static/front';
|
||||
req.query.skipSSLCheck = 'INVALID';
|
||||
|
||||
|
||||
@@ -35,6 +35,33 @@ describe('Challenge Model', () => {
|
||||
notes: 'test notes',
|
||||
},
|
||||
};
|
||||
const tasks2ToTest = {
|
||||
habit: {
|
||||
text: 'test habit 2',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 'test notes',
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo 2',
|
||||
type: 'todo',
|
||||
notes: 'test notes',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily 2',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
notes: 'test notes',
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward 2',
|
||||
type: 'reward',
|
||||
notes: 'test notes',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
guild = new Group({
|
||||
@@ -146,6 +173,60 @@ describe('Challenge Model', () => {
|
||||
expect(syncedTask.attribute).to.eql('str');
|
||||
});
|
||||
|
||||
it('should add challenge tag back to user upon syncing challenge tasks to a user with challenge tag removed', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
await challenge.syncTasksToUser(newMember);
|
||||
|
||||
let updatedNewMember = await User.findById(newMember._id).exec();
|
||||
const updatedNewMemberId = updatedNewMember._id;
|
||||
|
||||
updatedNewMember.tags = [];
|
||||
await updatedNewMember.save();
|
||||
|
||||
const taskValue2 = tasks2ToTest[taskType];
|
||||
const task2 = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue2));
|
||||
task2.challenge.id = challenge._id;
|
||||
await challenge.addTasks([task2]);
|
||||
await challenge.syncTasksToUser(updatedNewMember);
|
||||
|
||||
updatedNewMember = await User.findById(updatedNewMemberId).exec();
|
||||
|
||||
expect(updatedNewMember.tags.length).to.equal(1);
|
||||
expect(updatedNewMember.tags[0].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[0].name).to.equal(challenge.shortName);
|
||||
});
|
||||
|
||||
it('should not add a duplicate challenge tag to user upon syncing challenge tasks to a user with existing challenge tag', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
await challenge.syncTasksToUser(newMember);
|
||||
|
||||
let updatedNewMember = await User.findById(newMember._id).exec();
|
||||
const updatedNewMemberId = updatedNewMember._id;
|
||||
|
||||
const taskValue2 = tasks2ToTest[taskType];
|
||||
const task2 = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue2));
|
||||
task2.challenge.id = challenge._id;
|
||||
await challenge.addTasks([task2]);
|
||||
await challenge.syncTasksToUser(updatedNewMember);
|
||||
|
||||
updatedNewMember = await User.findById(updatedNewMemberId);
|
||||
|
||||
expect(updatedNewMember.tags.length).to.equal(8);
|
||||
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
||||
expect(updatedNewMember.tags.filter(tag => tag.id === challenge._id).length).to.equal(1);
|
||||
});
|
||||
|
||||
it('syncs challenge tasks to a user with the existing task', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
|
||||
@@ -235,15 +235,16 @@ describe('Group Task Methods', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('removes an assigned task and unlinks assignees', async () => {
|
||||
it('removes assigned tasks when master task is deleted', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.removeTask(task);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { model as Challenge } from '../../../../website/server/models/challenge'
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import { InternalServerError } from '../../../../website/server/libs/errors';
|
||||
import { generateHistory } from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('Task Model', () => {
|
||||
@@ -99,7 +98,8 @@ describe('Task Model', () => {
|
||||
throw new Error('No exception when Id is None');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.eql(new InternalServerError('Task identifier is a required argument'));
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
expect(err.message).to.eql('Task identifier is a required argument');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,7 +109,8 @@ describe('Task Model', () => {
|
||||
throw new Error('No exception when user_id is undefined');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.eql(new InternalServerError('User identifier is a required argument'));
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
expect(err.message).to.eql('User identifier is a required argument');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,6 +154,132 @@ describe('Task Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMultipleByIdOrAlias', () => {
|
||||
let taskWithAlias;
|
||||
let secondTask;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
await user.save();
|
||||
|
||||
taskWithAlias = new Tasks.todo({ // eslint-disable-line new-cap
|
||||
text: 'some text',
|
||||
alias: 'short-name',
|
||||
userId: user.id,
|
||||
});
|
||||
await taskWithAlias.save();
|
||||
|
||||
secondTask = new Tasks.habit({ // eslint-disable-line new-cap
|
||||
text: 'second task',
|
||||
alias: 'second-short-name',
|
||||
userId: user.id,
|
||||
});
|
||||
await secondTask.save();
|
||||
|
||||
sandbox.spy(Tasks.Task, 'find');
|
||||
});
|
||||
|
||||
it('throws an error if task identifiers is not passed in', async () => {
|
||||
try {
|
||||
await Tasks.Task.findMultipleByIdOrAlias(null, user._id);
|
||||
throw new Error('No exception when Id is None');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
expect(err.message).to.eql('Task identifiers is a required array argument');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if task identifiers is not an array', async () => {
|
||||
try {
|
||||
await Tasks.Task.findMultipleByIdOrAlias('string', user._id);
|
||||
throw new Error('No exception when Id is None');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
expect(err.message).to.eql('Task identifiers is a required array argument');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if user identifier is not passed in', async () => {
|
||||
try {
|
||||
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias._id]);
|
||||
throw new Error('No exception when user_id is undefined');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
expect(err.message).to.eql('User identifier is a required argument');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns task by id', async () => {
|
||||
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias._id], user._id);
|
||||
|
||||
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
|
||||
});
|
||||
|
||||
it('returns task by alias', async () => {
|
||||
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
|
||||
[taskWithAlias.alias], user._id,
|
||||
);
|
||||
|
||||
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
|
||||
});
|
||||
|
||||
it('returns multiple tasks', async () => {
|
||||
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
|
||||
[taskWithAlias.alias, secondTask._id], user._id,
|
||||
);
|
||||
|
||||
expect(foundTasks.length).to.eql(2);
|
||||
expect(foundTasks[0]._id).to.eql(taskWithAlias._id);
|
||||
expect(foundTasks[1]._id).to.eql(secondTask._id);
|
||||
});
|
||||
|
||||
it('returns a task only once if searched by both id and alias', async () => {
|
||||
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
|
||||
[taskWithAlias.alias, taskWithAlias._id], user._id,
|
||||
);
|
||||
|
||||
expect(foundTasks.length).to.eql(1);
|
||||
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
|
||||
});
|
||||
|
||||
it('scopes alias lookup to user', async () => {
|
||||
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id);
|
||||
|
||||
expect(Tasks.Task.find).to.be.calledOnce;
|
||||
expect(Tasks.Task.find).to.be.calledWithMatch({
|
||||
$or: [
|
||||
{ _id: { $in: [] } },
|
||||
{ alias: { $in: [taskWithAlias.alias] } },
|
||||
],
|
||||
userId: user._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array if tasks cannot be found', async () => {
|
||||
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(['not-found'], user._id);
|
||||
|
||||
expect(foundTasks).to.eql([]);
|
||||
});
|
||||
|
||||
it('accepts additional query parameters', async () => {
|
||||
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id, { foo: 'bar' });
|
||||
|
||||
expect(Tasks.Task.find).to.be.calledOnce;
|
||||
expect(Tasks.Task.find).to.be.calledWithMatch({
|
||||
$or: [
|
||||
{ _id: { $in: [] } },
|
||||
{ alias: { $in: [taskWithAlias.alias] } },
|
||||
],
|
||||
userId: user._id,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUserChallengeTask ', () => {
|
||||
});
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('days missed', () => {
|
||||
describe('daysUserHasMissed', () => {
|
||||
// http://forbrains.co.uk/international_tools/earth_timezones
|
||||
let user;
|
||||
|
||||
@@ -769,24 +769,51 @@ describe('User Model', () => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone', () => {
|
||||
const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const timezoneOffset = moment().zone('-06:00').zone();
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
it('correctly calculates days missed since lastCron', () => {
|
||||
const now = moment();
|
||||
user.lastCron = moment(now).subtract(5, 'days');
|
||||
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const req = {};
|
||||
req.header = () => timezoneOffset + 60;
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, req);
|
||||
expect(daysMissed).to.eql(5);
|
||||
});
|
||||
|
||||
it('uses timezone from preferences to calculate days missed', () => {
|
||||
const now = moment('2017-07-08 01:00:00Z');
|
||||
user.lastCron = moment('2017-07-04 13:00:00Z');
|
||||
user.preferences.timezoneOffset = 120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(3);
|
||||
});
|
||||
|
||||
it('uses timezone at last cron to calculate days missed', () => {
|
||||
const now = moment('2017-09-08 13:00:00Z');
|
||||
user.lastCron = moment('2017-09-06 01:00:00+02:00');
|
||||
user.preferences.timezoneOffset = 0;
|
||||
user.preferences.timezoneOffsetAtLastCron = -120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(2);
|
||||
});
|
||||
|
||||
it('respects new timezone that drags time into same day', () => {
|
||||
user.lastCron = moment('2017-12-05T00:00:00.000-06:00');
|
||||
user.preferences.timezoneOffset = 360;
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00');
|
||||
const requestWithMinus7Timezone = { header: () => 420 };
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, requestWithMinus7Timezone);
|
||||
|
||||
expect(user.preferences.timezoneOffset).to.eql(420);
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone with a custom day start', () => {
|
||||
const yesterday = moment('2017-12-05T02:00:00.000-08:00');
|
||||
const timezoneOffset = moment().zone('-08:00').zone();
|
||||
const timezoneOffset = 480;
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
user.preferences.dayStart = 2;
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not true', async () => {
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not true and req.query.limit is undefined', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
@@ -136,7 +136,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not defined', async () => {
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not defined and req.query.limit is undefined', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
@@ -155,6 +155,68 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is over 60', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', privacy: 'private' });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
const anotherUser = await generateUser();
|
||||
|
||||
await expect(anotherUser.get(`/challenges/${challenge._id}/members?limit=61`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is under 1', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', privacy: 'private' });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
const anotherUser = await generateUser();
|
||||
|
||||
await expect(anotherUser.get(`/challenges/${challenge._id}/members?limit=-13`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is not an integer', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', privacy: 'private' });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
const anotherUser = await generateUser();
|
||||
|
||||
await expect(anotherUser.get(`/challenges/${challenge._id}/members?limit=true`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns up to 60 members when req.query.limit is specified', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
const usersToGenerate = [];
|
||||
for (let i = 0; i < 62; i += 1) {
|
||||
usersToGenerate.push(generateUser({ challenges: [challenge._id] }));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get(`/challenges/${challenge._id}/members?limit=57`);
|
||||
expect(res.length).to.equal(57);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
res = await user.get(`/challenges/${challenge._id}/members?limit=60&lastId=${res[res.length - 1]._id}`);
|
||||
expect(res.length).to.equal(6);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
}).timeout(30000);
|
||||
|
||||
it('returns all members if req.query.includeAllMembers is true', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 = 2;
|
||||
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;
|
||||
@@ -236,11 +236,22 @@ describe('GET /groups', () => {
|
||||
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
|
||||
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
|
||||
const page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
|
||||
.to.eventually.have.a.lengthOf(1 + 4); // 1 created now, 4 by other tests
|
||||
expect(page2[4].name).to.equal('guild with less members');
|
||||
// 1 created now, 4 by other tests, -1 for no more tavern.
|
||||
.to.eventually.have.a.lengthOf(1 + 4 - 1);
|
||||
expect(page2[3].name).to.equal('guild with less members');
|
||||
}).timeout(10000);
|
||||
});
|
||||
|
||||
it('makes sure that the tavern doesn\'t show up when guilds is passed as a query', async () => {
|
||||
const guilds = await user.get('/groups?type=guilds');
|
||||
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
|
||||
});
|
||||
|
||||
it('makes sure that the tavern doesn\'t show up when publicGuilds is passed as a query', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds');
|
||||
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns all the user\'s guilds when guilds passed in as query', async () => {
|
||||
await expect(user.get('/groups?type=guilds'))
|
||||
.to.eventually.have.a
|
||||
@@ -254,7 +265,7 @@ describe('GET /groups', () => {
|
||||
|
||||
it('returns a list of groups user has access to', async () => {
|
||||
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
|
||||
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
|
||||
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern.
|
||||
});
|
||||
|
||||
it('returns a list of groups user has access to', async () => {
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 invites', async () => {
|
||||
it('returns only first 30 invites by default (req.query.limit not specified)', async () => {
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
|
||||
|
||||
@@ -89,6 +89,65 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
});
|
||||
}).timeout(10000);
|
||||
|
||||
it('returns an error if req.query.limit is over 60', async () => {
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
|
||||
|
||||
await expect(leader.get(`/groups/${group._id}/invites?limit=61`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is under 1', async () => {
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
|
||||
|
||||
await expect(leader.get(`/groups/${group._id}/invites?limit=-1`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is not an integer', async () => {
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
|
||||
|
||||
await expect(leader.get(`/groups/${group._id}/invites?limit=1.3`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns up to 60 invites when req.query.limit is specified', async () => {
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
|
||||
|
||||
const invitesToGenerate = [];
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
const generatedInvites = await Promise.all(invitesToGenerate);
|
||||
await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
|
||||
|
||||
let res = await leader.get(`/groups/${group._id}/invites?limit=14`);
|
||||
expect(res.length).to.equal(14);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
res = await leader.get(`/groups/${group._id}/invites?limit=31`);
|
||||
expect(res.length).to.equal(31);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
}).timeout(30000);
|
||||
|
||||
it('supports using req.query.lastId to get more invites', async function test () {
|
||||
this.timeout(30000); // @TODO: times out after 8 seconds
|
||||
const leader = await generateUser({ balance: 4 });
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
expect(memberRes.inbox.messages).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns only first 30 members', async () => {
|
||||
it('returns only first 30 members by default (req.query.limit not specified)', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
const usersToGenerate = [];
|
||||
@@ -133,6 +133,60 @@ describe('GET /groups/:groupId/members', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is over 60', async () => {
|
||||
await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
await expect(user.get('/groups/party/members?limit=61')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is under 1', async () => {
|
||||
await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
await expect(user.get('/groups/party/members?limit=0')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.query.limit is not an integer', async () => {
|
||||
await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
await expect(user.get('/groups/party/members?limit=1.1')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns up to 60 members when req.query.limit is specified', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
const usersToGenerate = [];
|
||||
for (let i = 0; i < 62; i += 1) {
|
||||
usersToGenerate.push(generateUser({ party: { _id: group._id } }));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get('/groups/party/members?limit=60');
|
||||
expect(res.length).to.equal(60);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
res = await user.get(`/groups/party/members?limit=60&lastId=${res[res.length - 1]._id}`);
|
||||
expect(res.length).to.equal(3);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
}).timeout(30000);
|
||||
|
||||
it('returns only first 30 members even when ?includeAllMembers=true', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
|
||||
|
||||
@@ -75,12 +75,7 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
|
||||
const memberTasks = await nonManager.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
|
||||
@@ -203,6 +203,16 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
|
||||
});
|
||||
|
||||
it('Issue #12291: accepting a redundant party invite will let the user stay in the party', async () => {
|
||||
await invitedUser.update({
|
||||
'party._id': party._id,
|
||||
});
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
|
||||
each(typesOfGroups, (groupDetails, groupType) => {
|
||||
context(`Leaving a group plan when the group is a ${groupType}`, () => {
|
||||
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
|
||||
let groupWithPlan;
|
||||
let leader;
|
||||
let member;
|
||||
@@ -341,6 +342,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
|
||||
each(typesOfGroups, (groupDetails, groupType) => {
|
||||
context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => {
|
||||
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
|
||||
const extraMonths = 12;
|
||||
let groupWithPlan;
|
||||
let member;
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('payments - stripe - #checkout', () => {
|
||||
stripePayments.checkout.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
it('creates a user subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
@@ -48,7 +48,7 @@ describe('payments - stripe - #checkout', () => {
|
||||
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
it('creates a group subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
|
||||
@@ -153,12 +153,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 420;
|
||||
const timezoneOffset = 420;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -180,12 +180,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 240;
|
||||
const timezoneOffset = 240;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -207,12 +207,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 540;
|
||||
const timezoneOffset = 540;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import apiError from '../../../../../website/server/libs/apiError';
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
@@ -44,7 +45,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/score/tt`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
message: apiError('directionUpDown'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,6 +262,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.completed).to.equal(true);
|
||||
expect(task.value).to.be.greaterThan(daily.value);
|
||||
});
|
||||
|
||||
it('uncompletes daily when direction is down', async () => {
|
||||
|
||||
@@ -499,6 +499,45 @@ describe('PUT /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('monthly dailys', () => {
|
||||
let monthly;
|
||||
|
||||
beforeEach(async () => {
|
||||
const date1 = moment.utc('2020-07-01').toDate();
|
||||
monthly = await user.post('/tasks/user', {
|
||||
text: 'test monthly',
|
||||
type: 'daily',
|
||||
frequency: 'monthly',
|
||||
startDate: date1,
|
||||
daysOfMonth: [date1.getDate()],
|
||||
});
|
||||
});
|
||||
|
||||
it('updates days of month when start date updated', async () => {
|
||||
const date2 = moment.utc('2020-07-01').toDate();
|
||||
const savedMonthly = await user.put(`/tasks/${monthly._id}`, {
|
||||
startDate: date2,
|
||||
});
|
||||
|
||||
expect(savedMonthly.daysOfMonth).to.deep.equal([moment(date2).date()]);
|
||||
});
|
||||
|
||||
it('updates next due when start date updated', async () => {
|
||||
const date2 = moment.utc('2022-07-01').toDate();
|
||||
const savedMonthly = await user.put(`/tasks/${monthly._id}`, {
|
||||
startDate: date2,
|
||||
});
|
||||
|
||||
expect(savedMonthly.nextDue.length).to.eql(6);
|
||||
expect(moment(savedMonthly.nextDue[0]).toDate()).to.eql(moment.utc('2022-08-01').toDate());
|
||||
expect(moment(savedMonthly.nextDue[1]).toDate()).to.eql(moment.utc('2022-09-01').toDate());
|
||||
expect(moment(savedMonthly.nextDue[2]).toDate()).to.eql(moment.utc('2022-10-01').toDate());
|
||||
expect(moment(savedMonthly.nextDue[3]).toDate()).to.eql(moment.utc('2022-11-01').toDate());
|
||||
expect(moment(savedMonthly.nextDue[4]).toDate()).to.eql(moment.utc('2022-12-01').toDate());
|
||||
expect(moment(savedMonthly.nextDue[5]).toDate()).to.eql(moment.utc('2023-01-01').toDate());
|
||||
});
|
||||
});
|
||||
|
||||
context('rewards', () => {
|
||||
let reward;
|
||||
|
||||
|
||||
@@ -73,12 +73,7 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -96,16 +91,16 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('unlinks assigned user', async () => {
|
||||
it('deletes task from assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks all assigned users', async () => {
|
||||
it('deletes task from all assigned users', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
@@ -114,8 +109,8 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(syncedTask).to.not.exist;
|
||||
expect(member2SyncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('prevents a user from deleting a task they are assigned to', async () => {
|
||||
@@ -130,22 +125,6 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a broken task', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
await member.del(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await expect(member.get(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Task not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a task after leaving a group', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
@@ -58,22 +58,14 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications[2].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -93,21 +85,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications[2].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -125,12 +109,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -157,14 +136,9 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
@@ -197,13 +171,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
@@ -226,13 +194,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
@@ -258,13 +220,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
@@ -287,21 +243,10 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
|
||||
|
||||
@@ -61,13 +61,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
@@ -114,12 +108,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
@@ -172,13 +161,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
|
||||
@@ -44,12 +44,11 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await user.sync();
|
||||
@@ -76,12 +75,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -111,31 +105,18 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskRequiresApproval'),
|
||||
});
|
||||
const response = await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskRequiresApproval'));
|
||||
});
|
||||
|
||||
it('allows a user to score an approved task', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
|
||||
@@ -71,12 +71,10 @@ describe('PUT /tasks/:id', () => {
|
||||
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
|
||||
|
||||
// score up to trigger approval
|
||||
await expect(member2.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
});
|
||||
|
||||
it('member updates a group task value - not allowed', async () => {
|
||||
|
||||
@@ -161,6 +161,23 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Issue #12361: returns an error if stealth has already been cast', async () => {
|
||||
await user.update({
|
||||
'stats.class': 'rogue',
|
||||
'stats.lvl': 15,
|
||||
'stats.mp': 400,
|
||||
'stats.buffs.stealth': 1,
|
||||
});
|
||||
await user.sync();
|
||||
await expect(user.post('/user/class/cast/stealth'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('spellAlreadyCast'),
|
||||
});
|
||||
expect(user.stats.mp).to.equal(400);
|
||||
});
|
||||
|
||||
it('returns an error if targeted party member doesn\'t exist', async () => {
|
||||
const { groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: { type: 'party', privacy: 'private' },
|
||||
|
||||
@@ -41,6 +41,29 @@ describe('POST /user/feed/:pet/:food', () => {
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(7);
|
||||
});
|
||||
|
||||
it('bulk feeding pet with non-preferred food', async () => {
|
||||
await user.update({
|
||||
'items.pets.Wolf-Base': 5,
|
||||
'items.food.Milk': 3,
|
||||
});
|
||||
|
||||
const food = content.food.Milk;
|
||||
const pet = content.petInfo['Wolf-Base'];
|
||||
|
||||
const res = await user.post('/user/feed/Wolf-Base/Milk?amount=2');
|
||||
await user.sync();
|
||||
expect(res).to.eql({
|
||||
data: user.items.pets['Wolf-Base'],
|
||||
message: t('messageDontEnjoyFood', {
|
||||
egg: pet.text(),
|
||||
foodText: food.textThe(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(user.items.food.Milk).to.eql(1);
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(9);
|
||||
});
|
||||
|
||||
context('sending user activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
@@ -77,5 +100,33 @@ describe('POST /user/feed/:pet/:food', () => {
|
||||
expect(body.pet).to.eql('Wolf-Base');
|
||||
expect(body.message).to.eql(res.message);
|
||||
});
|
||||
|
||||
it('sends user activity webhook (mount raised after full bulk feeding)', async () => {
|
||||
const uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'userActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
mountRaised: true,
|
||||
},
|
||||
});
|
||||
|
||||
await user.update({
|
||||
'items.pets.Wolf-Base': 47,
|
||||
'items.food.Milk': 3,
|
||||
});
|
||||
const res = await user.post('/user/feed/Wolf-Base/Milk?amount=2');
|
||||
|
||||
await sleep();
|
||||
|
||||
const body = server.getWebhookData(uuid);
|
||||
|
||||
expect(user.achievements.allYourBase).to.not.equal(true);
|
||||
expect(body.type).to.eql('mountRaised');
|
||||
expect(body.pet).to.eql('Wolf-Base');
|
||||
expect(body.message).to.eql(res.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,14 @@ describe('PUT /user', () => {
|
||||
error: 'BadRequest',
|
||||
message: t('displaynameIssueSlur'),
|
||||
});
|
||||
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': 'namecontainsnewline\n',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('displaynameIssueNewline'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -127,19 +127,26 @@ describe('PUT /user/webhook/:id', () => {
|
||||
it('can update taskActivity options', async () => {
|
||||
const type = 'taskActivity';
|
||||
const options = {
|
||||
checklistScored: true,
|
||||
updated: false,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
};
|
||||
|
||||
const webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, { type, options });
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
checklistScored: false, // starting value
|
||||
const expected = {
|
||||
checklistScored: true,
|
||||
created: true, // starting value
|
||||
updated: false,
|
||||
deleted: true,
|
||||
scored: true, // default value
|
||||
});
|
||||
deleted: false, // starting value
|
||||
scored: false,
|
||||
};
|
||||
|
||||
const returnedWebhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, { type, options });
|
||||
|
||||
await user.sync();
|
||||
|
||||
const savedWebhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(returnedWebhook.options).to.eql(expected);
|
||||
expect(savedWebhook.options).to.eql(expected);
|
||||
});
|
||||
|
||||
it('errors if taskActivity option is not a boolean', async () => {
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
translate as t,
|
||||
server,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('POST /tasks/bulk-score', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
});
|
||||
});
|
||||
|
||||
context('all', () => {
|
||||
it('can use id to identify the task', async () => {
|
||||
const todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
alias: 'alias',
|
||||
});
|
||||
|
||||
const res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
|
||||
expect(res).to.be.ok;
|
||||
expect(res.tasks.length).to.equal(1);
|
||||
expect(res.tasks[0].id).to.equal(todo._id);
|
||||
expect(res.tasks[0].delta).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('can use a alias in place of the id', async () => {
|
||||
const todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
alias: 'alias',
|
||||
});
|
||||
|
||||
const res = await user.post('/tasks/bulk-score', [{ id: todo.alias, direction: 'up' }]);
|
||||
|
||||
expect(res).to.be.ok;
|
||||
expect(res.tasks.length).to.equal(1);
|
||||
expect(res.tasks[0].id).to.equal(todo._id);
|
||||
expect(res.tasks[0].delta).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('sends task scored webhooks', async () => {
|
||||
const uuid = generateUUID();
|
||||
await server.start();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
scored: true,
|
||||
},
|
||||
});
|
||||
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post('/tasks/bulk-score', [{ id: task.id, direction: 'up' }]);
|
||||
|
||||
await sleep();
|
||||
|
||||
await server.close();
|
||||
|
||||
const body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.user).to.have.all.keys('_id', '_tmp', 'stats');
|
||||
expect(body.user.stats).to.have.all.keys('hp', 'mp', 'exp', 'gp', 'lvl', 'class', 'points', 'str', 'con', 'int', 'per', 'buffs', 'training', 'maxHealth', 'maxMP', 'toNextLevel');
|
||||
expect(body.task.id).to.eql(task.id);
|
||||
expect(body.direction).to.eql('up');
|
||||
expect(body.delta).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
context('sending user activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends user activity webhook when the user levels up', async () => {
|
||||
const uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'userActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
leveledUp: true,
|
||||
},
|
||||
});
|
||||
|
||||
const initialLvl = user.stats.lvl;
|
||||
|
||||
await user.update({
|
||||
'stats.exp': 3000,
|
||||
});
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post('/tasks/bulk-score', [{ id: task.id, direction: 'up' }]);
|
||||
await user.sync();
|
||||
await sleep();
|
||||
|
||||
const body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.type).to.eql('leveledUp');
|
||||
expect(body.initialLvl).to.eql(initialLvl);
|
||||
expect(body.finalLvl).to.eql(user.stats.lvl);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails the entire op if one task scoring fails', async () => {
|
||||
const todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
});
|
||||
const habit = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await expect(user.post('/tasks/bulk-score', [
|
||||
{ id: todo.id, direction: 'down' },
|
||||
{ id: habit.id, direction: 'down' },
|
||||
])).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
|
||||
const updatedHabit = await user.get(`/tasks/${habit._id}`);
|
||||
expect(updatedHabit.history.length).to.equal(0);
|
||||
expect(updatedHabit.value).to.equal(0);
|
||||
|
||||
const updatedTodo = await user.get(`/tasks/${todo._id}`);
|
||||
expect(updatedTodo.value).to.equal(0);
|
||||
});
|
||||
|
||||
it('sends _tmp for each task', async () => {
|
||||
const habit1 = await user.post('/tasks/user', {
|
||||
text: 'test habit 1',
|
||||
type: 'habit',
|
||||
});
|
||||
const habit2 = await user.post('/tasks/user', {
|
||||
text: 'test habit 2',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.update({
|
||||
'party.quest.key': 'gryphon',
|
||||
});
|
||||
|
||||
const res = await user.post('/tasks/bulk-score', [
|
||||
{ id: habit1._id, direction: 'up' },
|
||||
{ id: habit2._id, direction: 'up' },
|
||||
]);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(res.tasks[0]._tmp.quest.progressDelta).to.be.greaterThan(0);
|
||||
expect(res.tasks[1]._tmp.quest.progressDelta).to.be.greaterThan(0);
|
||||
expect(user.party.quest.progress.up).to
|
||||
.eql(res.tasks[0]._tmp.quest.progressDelta + res.tasks[1]._tmp.quest.progressDelta);
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
let todo;
|
||||
|
||||
beforeEach(async () => {
|
||||
todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
});
|
||||
});
|
||||
|
||||
it('completes todo when direction is up', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
const task = await user.get(`/tasks/${todo._id}`);
|
||||
|
||||
expect(task.completed).to.equal(true);
|
||||
expect(task.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('moves completed todos out of user.tasksOrder.todos', async () => {
|
||||
const getUser = await user.get('/user');
|
||||
expect(getUser.tasksOrder.todos.indexOf(todo._id)).to.not.equal(-1);
|
||||
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
const updatedTask = await user.get(`/tasks/${todo._id}`);
|
||||
expect(updatedTask.completed).to.equal(true);
|
||||
|
||||
const updatedUser = await user.get('/user');
|
||||
expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).to.equal(-1);
|
||||
});
|
||||
|
||||
it('moves un-completed todos back into user.tasksOrder.todos', async () => {
|
||||
const getUser = await user.get('/user');
|
||||
expect(getUser.tasksOrder.todos.indexOf(todo._id)).to.not.equal(-1);
|
||||
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }]);
|
||||
|
||||
const updatedTask = await user.get(`/tasks/${todo._id}`);
|
||||
expect(updatedTask.completed).to.equal(false);
|
||||
|
||||
const updatedUser = await user.get('/user');
|
||||
const l = updatedUser.tasksOrder.todos.length;
|
||||
expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).not.to.equal(-1);
|
||||
// Check that it was pushed at the bottom
|
||||
expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).to.equal(l - 1);
|
||||
});
|
||||
|
||||
it('uncompletes todo when direction is down', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }, { id: todo.id, direction: 'down' }]);
|
||||
const updatedTask = await user.get(`/tasks/${todo._id}`);
|
||||
|
||||
expect(updatedTask.completed).to.equal(false);
|
||||
expect(updatedTask.dateCompleted).to.be.a('undefined');
|
||||
});
|
||||
|
||||
it('doesn\'t let a todo be uncompleted twice', async () => {
|
||||
await expect(user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }])).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser; let res;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('increases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
});
|
||||
|
||||
it('increases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('increases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser; let initialUser; let res;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]);
|
||||
initialUser = await user.get('/user');
|
||||
res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }]);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('dailys', () => {
|
||||
let daily;
|
||||
|
||||
beforeEach(async () => {
|
||||
daily = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
});
|
||||
|
||||
it('completes daily when direction is up', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]);
|
||||
const task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('uncompletes daily when direction is down', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }, { id: daily.id, direction: 'down' }]);
|
||||
const task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('computes isDue', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]);
|
||||
const task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.isDue).to.equal(true);
|
||||
});
|
||||
|
||||
it('computes nextDue', async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]);
|
||||
const task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser; let res;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('increases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
});
|
||||
|
||||
it('increases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('increases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser; let initialUser; let res;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]);
|
||||
initialUser = await user.get('/user');
|
||||
res = await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'down' }]);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
let habit; let minusHabit; let plusHabit; let
|
||||
neitherHabit; // eslint-disable-line no-unused-vars
|
||||
|
||||
beforeEach(async () => {
|
||||
habit = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
minusHabit = await user.post('/tasks/user', {
|
||||
text: 'test min habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
});
|
||||
|
||||
plusHabit = await user.post('/tasks/user', {
|
||||
text: 'test plus habit',
|
||||
type: 'habit',
|
||||
down: false,
|
||||
});
|
||||
|
||||
neitherHabit = await user.post('/tasks/user', {
|
||||
text: 'test neither habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('increases user\'s mp when direction is up', async () => {
|
||||
const res = await user.post('/tasks/bulk-score', [{ id: habit.id, direction: 'up' }, {
|
||||
id: plusHabit.id,
|
||||
direction: 'up',
|
||||
}]);
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s mp when direction is down', async () => {
|
||||
const res = await user.post('/tasks/bulk-score', [{
|
||||
id: habit.id,
|
||||
direction: 'down',
|
||||
}, {
|
||||
id: minusHabit.id,
|
||||
direction: 'down',
|
||||
}]);
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
});
|
||||
|
||||
it('increases user\'s exp when direction is up', async () => {
|
||||
const res = await user.post('/tasks/bulk-score', [{
|
||||
id: habit.id,
|
||||
direction: 'up',
|
||||
}, {
|
||||
id: plusHabit.id,
|
||||
direction: 'up',
|
||||
}]);
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('increases user\'s gold when direction is up', async () => {
|
||||
const res = await user.post('/tasks/bulk-score', [{
|
||||
id: habit.id,
|
||||
direction: 'up',
|
||||
}, {
|
||||
id: plusHabit.id,
|
||||
direction: 'up',
|
||||
}]);
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
|
||||
it('records only one history entry per day', async () => {
|
||||
const initialHistoryLength = habit.history.length;
|
||||
await user.post('/tasks/bulk-score', [{
|
||||
id: habit.id,
|
||||
direction: 'up',
|
||||
}, {
|
||||
id: habit.id,
|
||||
direction: 'up',
|
||||
}, {
|
||||
id: habit.id,
|
||||
direction: 'down',
|
||||
}, {
|
||||
id: habit.id,
|
||||
direction: 'up',
|
||||
}]);
|
||||
|
||||
const updatedTask = await user.get(`/tasks/${habit._id}`);
|
||||
|
||||
expect(updatedTask.history.length).to.eql(initialHistoryLength + 1);
|
||||
|
||||
const lastHistoryEntry = updatedTask.history[updatedTask.history.length - 1];
|
||||
expect(lastHistoryEntry.scoredUp).to.equal(3);
|
||||
expect(lastHistoryEntry.scoredDown).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
context('mixed', () => {
|
||||
let habit; let daily; let todo;
|
||||
|
||||
beforeEach(async () => {
|
||||
habit = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
daily = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
todo = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
});
|
||||
|
||||
it('scores habits, dailies, todos', async () => {
|
||||
const res = await user.post('/tasks/bulk-score', [
|
||||
{ id: habit.id, direction: 'down' },
|
||||
{ id: daily.id, direction: 'up' },
|
||||
{ id: todo.id, direction: 'up' },
|
||||
]);
|
||||
|
||||
expect(res.tasks[0].id).to.eql(habit.id);
|
||||
expect(res.tasks[0].delta).to.be.below(0);
|
||||
expect(res.tasks[0]._tmp).to.exist;
|
||||
|
||||
expect(res.tasks[1].id).to.eql(daily.id);
|
||||
expect(res.tasks[1].delta).to.be.greaterThan(0);
|
||||
expect(res.tasks[1]._tmp).to.exist;
|
||||
|
||||
expect(res.tasks[2].id).to.eql(todo.id);
|
||||
expect(res.tasks[2].delta).to.be.greaterThan(0);
|
||||
expect(res.tasks[2]._tmp).to.exist;
|
||||
|
||||
const updatedHabit = await user.get(`/tasks/${habit._id}`);
|
||||
const updatedDaily = await user.get(`/tasks/${daily._id}`);
|
||||
const updatedTodo = await user.get(`/tasks/${todo._id}`);
|
||||
|
||||
expect(habit.value).to.be.greaterThan(updatedHabit.value);
|
||||
expect(updatedHabit.counterDown).to.equal(1);
|
||||
expect(updatedDaily.value).to.be.greaterThan(daily.value);
|
||||
expect(updatedTodo.value).to.be.greaterThan(todo.value);
|
||||
});
|
||||
});
|
||||
|
||||
context('reward', () => {
|
||||
it('correctly handles rewards', async () => {
|
||||
const reward = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
value: 5,
|
||||
});
|
||||
|
||||
const res = await user.post('/tasks/bulk-score', [{ id: reward.id, direction: 'up' }]);
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
// purchases reward
|
||||
expect(user.stats.gp).to.equal(updatedUser.stats.gp + 5);
|
||||
expect(res.gp).to.equal(updatedUser.stats.gp);
|
||||
|
||||
// does not change user\'s mp
|
||||
expect(user.stats.mp).to.equal(updatedUser.stats.mp);
|
||||
expect(res.mp).to.equal(updatedUser.stats.mp);
|
||||
|
||||
// does not change user\'s exp
|
||||
expect(user.stats.exp).to.equal(updatedUser.stats.exp);
|
||||
expect(res.exp).to.equal(updatedUser.stats.exp);
|
||||
});
|
||||
|
||||
it('fails if the user does not have enough gold', async () => {
|
||||
const reward = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
value: 500,
|
||||
});
|
||||
|
||||
await expect(user.post('/tasks/bulk-score', [{ id: reward.id, direction: 'up' }])).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageNotEnoughGold'),
|
||||
});
|
||||
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
// does not purchase reward
|
||||
expect(user.stats.gp).to.equal(updatedUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,5 +53,11 @@ describe('POST /user/auth/verify-display-name', async () => {
|
||||
displayName: 'this is a very long display name over 30 characters',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength')] });
|
||||
});
|
||||
|
||||
it('errors if display name contains a newline', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
displayName: 'namecontainsnewline\n',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueNewline')] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
|
||||
describe('getUtcOffset', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = { preferences: {} };
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is not set', () => {
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is zero', () => {
|
||||
user.preferences.timezoneOffset = 0;
|
||||
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns the opposite of user.timezoneOffset', () => {
|
||||
user.preferences.timezoneOffset = -10;
|
||||
|
||||
expect(getUtcOffset(user)).to.eql(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { startOfDay, daysSince } from '../../../website/common/script/cron';
|
||||
|
||||
function localMoment (timeString, utcOffset) {
|
||||
return moment(timeString).utcOffset(utcOffset, true);
|
||||
}
|
||||
|
||||
describe('cron utility functions', () => {
|
||||
describe('startOfDay', () => {
|
||||
it('is zero when no daystart configured', () => {
|
||||
const options = { now: moment('2020-02-02 09:30:00Z'), timezoneOffset: 0 };
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when negative daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: -5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when daystart over 24 is configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: 25,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is equal to daystart o\'clock when daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is previous day daystart o\'clock when daystart is after current time', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-01 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is daystart o\'clock when daystart is after current time due to timezone', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: -120,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00+02:00');
|
||||
});
|
||||
|
||||
it('returns in default timezone if no timezone defined', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const now = localMoment('2020-02-02 04:30:00', utcOffset).utc();
|
||||
|
||||
const result = startOfDay({ now });
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone lower than -12:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 17:30:00', utcOffset).utc(),
|
||||
timezoneOffset: 721,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone higher than +14:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 07:32:25.376', utcOffset).utc(),
|
||||
timezoneOffset: -841,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in overridden timezone if override present', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 13:30:27Z'),
|
||||
timezoneOffset: 0,
|
||||
timezoneUtcOffsetOverride: -240,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00-04:00');
|
||||
});
|
||||
|
||||
it('returns start of yesterday if timezone difference carries it over datelines', () => {
|
||||
const offset = 300;
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: offset,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-01', -offset));
|
||||
});
|
||||
});
|
||||
|
||||
describe('daysSince', () => {
|
||||
it('correctly calculates days between two dates', () => {
|
||||
const now = moment();
|
||||
const dayBeforeYesterday = moment(now).subtract({ days: 2 });
|
||||
|
||||
expect(daysSince(dayBeforeYesterday, { now })).to.equal(2);
|
||||
});
|
||||
|
||||
it('is one lower if current time is before dayStart', () => {
|
||||
const oneWeekAgoAtOnePm = moment().hour(13).subtract({ days: 7 });
|
||||
const thisMorningThreeAm = moment().hour(3);
|
||||
const options = {
|
||||
now: thisMorningThreeAm,
|
||||
dayStart: 6,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtOnePm, options);
|
||||
|
||||
expect(result).to.equal(6);
|
||||
});
|
||||
|
||||
it('is one higher if reference time is before dayStart and current time after dayStart', () => {
|
||||
const oneWeekAgoAtEightAm = moment().hour(8).subtract({ days: 7 });
|
||||
const todayAtFivePm = moment().hour(17);
|
||||
const options = {
|
||||
now: todayAtFivePm,
|
||||
dayStart: 11,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtEightAm, options);
|
||||
|
||||
expect(result).to.equal(8);
|
||||
});
|
||||
|
||||
// Variations in timezone configuration options are already covered by startOfDay tests.
|
||||
it('uses now in user timezone as configured in options', () => {
|
||||
const timezoneOffset = 120;
|
||||
const options = {
|
||||
now: moment('1989-11-09 02:53:00+01:00'),
|
||||
timezoneOffset,
|
||||
};
|
||||
|
||||
const result = daysSince(localMoment('1989-11-08', -timezoneOffset), options);
|
||||
|
||||
expect(result).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import taskDefaults from '../../../website/common/script/libs/taskDefaults';
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
|
||||
describe('taskDefaults', () => {
|
||||
@@ -72,7 +73,7 @@ describe('taskDefaults', () => {
|
||||
|
||||
expect(task.startDate).to.eql(
|
||||
moment()
|
||||
.zone(user.preferences.timezoneOffset, 'hour')
|
||||
.utcOffset(getUtcOffset(user))
|
||||
.startOf('day')
|
||||
.subtract(1, 'day')
|
||||
.toDate(),
|
||||
|
||||
@@ -113,6 +113,30 @@ describe('shared.ops.feed', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not allow bulk-feeding query amount above food owned', done => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 6;
|
||||
try {
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 8 } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughFood'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not allow bulk-over-feeding pet', done => {
|
||||
user.items.pets['Wolf-Base'] = 45;
|
||||
user.items.food.Meat = 3;
|
||||
try {
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 2 } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('tooMuchFood'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('successful feeding', () => {
|
||||
@@ -188,6 +212,61 @@ describe('shared.ops.feed', () => {
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(7);
|
||||
});
|
||||
|
||||
it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50 preferred food (bulk)', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 10;
|
||||
user.items.currentPet = 'Wolf-Base';
|
||||
|
||||
const pet = content.petInfo['Wolf-Base'];
|
||||
|
||||
const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 9 } });
|
||||
expect(data).to.eql(user.items.pets['Wolf-Base']);
|
||||
expect(message).to.eql(i18n.t('messageEvolve', {
|
||||
egg: pet.text(),
|
||||
}));
|
||||
|
||||
expect(user.items.food.Meat).to.equal(1);
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(-1);
|
||||
expect(user.items.mounts['Wolf-Base']).to.equal(true);
|
||||
expect(user.items.currentPet).to.equal('');
|
||||
});
|
||||
|
||||
it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50 wrong food (bulk)', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Milk = 25;
|
||||
user.items.currentPet = 'Wolf-Base';
|
||||
|
||||
const pet = content.petInfo['Wolf-Base'];
|
||||
|
||||
const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Milk' }, query: { amount: 23 } });
|
||||
expect(data).to.eql(user.items.pets['Wolf-Base']);
|
||||
expect(message).to.eql(i18n.t('messageEvolve', {
|
||||
egg: pet.text(),
|
||||
}));
|
||||
expect(user.items.food.Milk).to.equal(2);
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(-1);
|
||||
expect(user.items.mounts['Wolf-Base']).to.equal(true);
|
||||
expect(user.items.currentPet).to.equal('');
|
||||
});
|
||||
|
||||
it('does not like the food (bulk low food) ', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Milk = 5;
|
||||
|
||||
const food = content.food.Milk;
|
||||
const pet = content.petInfo['Wolf-Base'];
|
||||
|
||||
const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Milk' }, query: { amount: 5 } });
|
||||
expect(data).to.eql(user.items.pets['Wolf-Base']);
|
||||
expect(message).to.eql(i18n.t('messageDontEnjoyFood', {
|
||||
egg: pet.text(),
|
||||
foodText: food.textThe(),
|
||||
}));
|
||||
|
||||
expect(user.items.food.Milk).to.equal(0);
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(15);
|
||||
});
|
||||
|
||||
it('awards All Your Base achievement', () => {
|
||||
user.items.pets['Wolf-Spooky'] = 5;
|
||||
user.items.food.Milk = 2;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
import spells from '../../../website/common/script/content/spells';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
|
||||
@@ -25,7 +26,7 @@ describe('shared.ops.spells', () => {
|
||||
const spell = spells.healer.heal;
|
||||
|
||||
try {
|
||||
spell.cast(user);
|
||||
spell.cast(user, null, { language: 'en' });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
|
||||
@@ -35,4 +36,22 @@ describe('shared.ops.spells', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('Issue #12361: returns an error if chilling frost has already been cast', done => {
|
||||
user.stats.class = 'wizard';
|
||||
user.stats.lvl = 15;
|
||||
user.stats.mp = 400;
|
||||
user.stats.buffs.streaks = true;
|
||||
|
||||
const spell = spells.wizard.frost;
|
||||
try {
|
||||
spell.cast(user, null, { language: 'en' });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('spellAlreadyCast'));
|
||||
expect(user.stats.mp).to.eql(400);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'moment-recur';
|
||||
describe('shouldDo', () => {
|
||||
let day; let
|
||||
dailyTask;
|
||||
// Options is a mapping of user.preferences, therefor `timezoneOffset` still holds old zone
|
||||
// values instead of utcOffset values.
|
||||
let options = {};
|
||||
let nextDue = [];
|
||||
|
||||
@@ -80,17 +82,17 @@ describe('shouldDo', () => {
|
||||
|
||||
it('returns true if the user\'s current time is after start date and Custom Day Start', () => {
|
||||
options.dayStart = 4;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if the user\'s current time is before Custom Day Start', () => {
|
||||
options.dayStart = 8;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -112,14 +114,14 @@ describe('shouldDo', () => {
|
||||
|
||||
it('returns true if the user\'s current time is after Custom Day Start', () => {
|
||||
options.dayStart = 4;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if the user\'s current time is before Custom Day Start', () => {
|
||||
options.dayStart = 8;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//------------------------------
|
||||
global._ = require('lodash');
|
||||
global.chai = require('chai');
|
||||
chai.use(require('sinon-chai'));
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('chai-moment'));
|
||||
chai.use(require('sinon-chai'));
|
||||
|
||||
global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { configure } from '@storybook/vue';
|
||||
import './margin.css';
|
||||
import '../../src/assets/scss/index.scss';
|
||||
import '../../src/assets/css/sprites.css';
|
||||
|
||||
@@ -35,7 +36,7 @@ import BootstrapVue from 'bootstrap-vue';
|
||||
import StoreModule from '@/libs/store';
|
||||
|
||||
// couldn't inject the languages easily,
|
||||
// so just a "$t()" string to show that this will be translated
|
||||
// so just a "$t()" string to show that this will be translated
|
||||
Vue.prototype.$t = function translateString (...args) {
|
||||
return `$t(${JSON.stringify(args)})`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.background {
|
||||
background: teal;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: white;
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -16,20 +16,20 @@
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-knobs": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.21",
|
||||
"@storybook/vue": "^5.3.19",
|
||||
"@vue/cli-plugin-babel": "^4.4.6",
|
||||
"@vue/cli-plugin-eslint": "^4.4.6",
|
||||
"@vue/cli-plugin-router": "^4.4.6",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.4.6",
|
||||
"@vue/cli-service": "^4.4.6",
|
||||
"@vue/cli-plugin-babel": "^4.5.4",
|
||||
"@vue/cli-plugin-eslint": "^4.5.4",
|
||||
"@vue/cli-plugin-router": "^4.5.4",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^6.2.0",
|
||||
"amplitude-js": "^7.1.1",
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.5.0",
|
||||
"bootstrap-vue": "^2.15.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"bootstrap-vue": "^2.16.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -41,25 +41,25 @@
|
||||
"inspectpack": "^4.5.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.27.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.9",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.16.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^8.2.0",
|
||||
"uuid": "^8.3.0",
|
||||
"validator": "^13.1.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue": "^2.6.12",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.3.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.43.0"
|
||||
"vue-router": "^3.4.3",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.44.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
#melior {
|
||||
margin: 0 auto;
|
||||
width: 70.9px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -218,11 +217,6 @@
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
||||
.modal {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: .9 !important;
|
||||
background-color: $purple-100 !important;
|
||||
@@ -297,7 +291,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
@@ -369,7 +363,6 @@ export default {
|
||||
|
||||
const isApiCall = url.indexOf('api/v4') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
@@ -381,9 +374,14 @@ export default {
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v4/cron because the user is synced automatically after cron runs
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
// exclude skills casting as they already return the synced user
|
||||
const isCast = url.indexOf('/api/v4/user/class/cast') !== -1 && method === 'post';
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
if (
|
||||
userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync && !isCast
|
||||
) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
@@ -489,9 +487,10 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
|
||||
// Adjust the timezone offset
|
||||
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
|
||||
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
|
||||
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.timezoneOffset': this.browserTimezoneOffset,
|
||||
'preferences.timezoneOffset': browserTimezoneOffset,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -513,13 +512,9 @@ export default {
|
||||
} else {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
|
||||
this.initializeModalStack();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('playSound');
|
||||
this.$root.$off('bv::modal::hidden');
|
||||
this.$root.$off('bv::show::modal');
|
||||
this.$root.$off('buyModal::showItem');
|
||||
this.$root.$off('selectMembersModal::showItem');
|
||||
},
|
||||
@@ -549,112 +544,6 @@ export default {
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return true;
|
||||
},
|
||||
initializeModalStack () {
|
||||
// Manage modals
|
||||
this.$root.$on('bv::show::modal', (modalId, data = {}) => {
|
||||
if (data.fromRoot) return;
|
||||
const { modalStack } = this.$store.state;
|
||||
|
||||
this.trackGemPurchase(modalId, data);
|
||||
|
||||
// Add new modal to the stack
|
||||
const prev = modalStack[modalStack.length - 1];
|
||||
const prevId = prev ? prev.modalId : undefined;
|
||||
modalStack.push({ modalId, prev: prevId });
|
||||
});
|
||||
|
||||
this.$root.$on('bv::modal::hidden', bvEvent => {
|
||||
let modalId = bvEvent.target && bvEvent.target.id;
|
||||
|
||||
// sometimes the target isn't passed to the hidden event, fallback is the vueTarget
|
||||
if (!modalId) {
|
||||
modalId = bvEvent.vueTarget && bvEvent.vueTarget.id;
|
||||
}
|
||||
|
||||
if (!modalId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { modalStack } = this.$store.state;
|
||||
|
||||
const modalOnTop = modalStack[modalStack.length - 1];
|
||||
|
||||
// Check for invalid modal. Event systems can send multiples
|
||||
if (!this.validStack(modalStack)) return;
|
||||
|
||||
// If we are moving forward
|
||||
if (modalOnTop && modalOnTop.prev === modalId) return;
|
||||
|
||||
// Remove modal from stack
|
||||
this.$store.state.modalStack.pop();
|
||||
|
||||
// Get previous modal
|
||||
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
|
||||
|
||||
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, { fromRoot: true });
|
||||
});
|
||||
|
||||
// Dismiss modal aggressively. Pass a modal ID to remove a modal instance from the stack
|
||||
// (both the stack entry itself and its "prev" reference) so we don't reopen it
|
||||
this.$root.$on('habitica::dismiss-modal', oldModal => {
|
||||
if (!oldModal) return;
|
||||
this.$root.$emit('bv::hide::modal', oldModal);
|
||||
let removeIndex = this.$store.state.modalStack
|
||||
.map(modal => modal.modalId)
|
||||
.indexOf(oldModal);
|
||||
if (removeIndex >= 0) {
|
||||
this.$store.state.modalStack.splice(removeIndex, 1);
|
||||
}
|
||||
removeIndex = this.$store.state.modalStack
|
||||
.map(modal => modal.prev)
|
||||
.indexOf(oldModal);
|
||||
if (removeIndex >= 0) {
|
||||
delete this.$store.state.modalStack[removeIndex].prev;
|
||||
}
|
||||
});
|
||||
},
|
||||
validStack (modalStack) {
|
||||
const modalsThatCanShowTwice = ['profile'];
|
||||
const modalCount = {};
|
||||
const prevAndCurrent = 2;
|
||||
|
||||
for (const current of modalStack) {
|
||||
if (!modalCount[current.modalId]) modalCount[current.modalId] = 0;
|
||||
modalCount[current.modalId] += 1;
|
||||
if (
|
||||
modalCount[current.modalId] > prevAndCurrent
|
||||
&& modalsThatCanShowTwice.indexOf(current.modalId) === -1
|
||||
) {
|
||||
this.$store.state.modalStack = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current.prev) continue; // eslint-disable-line
|
||||
if (!modalCount[current.prev]) modalCount[current.prev] = 0;
|
||||
modalCount[current.prev] += 1;
|
||||
if (
|
||||
modalCount[current.prev] > prevAndCurrent
|
||||
&& modalsThatCanShowTwice.indexOf(current.prev) === -1
|
||||
) {
|
||||
this.$store.state.modalStack = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
trackGemPurchase (modalId, data) {
|
||||
// Track opening of gems modal unless it's been already tracked
|
||||
// For example the gems button in the menu already tracks the event by itself
|
||||
if (modalId === 'buy-gems' && data.alreadyTracked !== true) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Wallet',
|
||||
});
|
||||
}
|
||||
},
|
||||
itemSelected (item) {
|
||||
this.selectedItemToBuy = item;
|
||||
},
|
||||
|
||||
@@ -4,16 +4,27 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
.quest_windup {
|
||||
background: url("~@/assets/images/animated/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
.promo_armoire_backgrounds_202007 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -340px -524px;
|
||||
background-position: -376px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202007 {
|
||||
.promo_armoire_backgrounds_202008 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -735px;
|
||||
background-position: -376px -148px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_armoire_backgrounds_202009 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -383px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_golden_achievements {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -295px -531px;
|
||||
width: 246px;
|
||||
height: 112px;
|
||||
}
|
||||
.promo_mystery_202008 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -531px;
|
||||
width: 294px;
|
||||
height: 156px;
|
||||
}
|
||||
.promo_mystery_202009 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -383px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_sand_sculpture_potions {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -735px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_splashy_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -409px -337px;
|
||||
width: 375px;
|
||||
height: 186px;
|
||||
}
|
||||
.customize-option.promo_splashy_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -434px -352px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_summer_splash_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -337px;
|
||||
width: 408px;
|
||||
height: 186px;
|
||||
}
|
||||
.promo_summer_splash_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -445px 0px;
|
||||
width: 444px;
|
||||
height: 198px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1032px -389px;
|
||||
background-position: -800px -537px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_achievement {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -524px;
|
||||
width: 339px;
|
||||
height: 210px;
|
||||
}
|
||||
.scene_hat_guild {
|
||||
.promo_time_travelers {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 444px;
|
||||
height: 336px;
|
||||
width: 375px;
|
||||
height: 186px;
|
||||
}
|
||||
.scene_hiking {
|
||||
.scene_CernyPie {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -890px 0px;
|
||||
width: 258px;
|
||||
height: 258px;
|
||||
}
|
||||
.scene_nakonana {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -890px -389px;
|
||||
background-position: 0px -688px;
|
||||
width: 141px;
|
||||
height: 169px;
|
||||
height: 167px;
|
||||
}
|
||||
.scene_strength {
|
||||
.scene_achievement {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -890px -259px;
|
||||
width: 192px;
|
||||
height: 129px;
|
||||
background-position: -800px 0px;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
}
|
||||
.scene_public_space {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -187px;
|
||||
width: 345px;
|
||||
height: 195px;
|
||||
}
|
||||
.scene_reading {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -800px -392px;
|
||||
width: 171px;
|
||||
height: 144px;
|
||||
}
|
||||
.scene_rewards {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -800px -211px;
|
||||
width: 207px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
@@ -1,396 +1,756 @@
|
||||
.Pet_Currency_Gem {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1344px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_Currency_Gem1x {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1477px -1287px;
|
||||
width: 15px;
|
||||
height: 13px;
|
||||
}
|
||||
.Pet_Currency_Gem2x {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1025px -838px;
|
||||
width: 30px;
|
||||
height: 26px;
|
||||
}
|
||||
.PixelPaw-Gold {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -473px -220px;
|
||||
width: 51px;
|
||||
height: 51px;
|
||||
}
|
||||
.PixelPaw {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -473px -272px;
|
||||
width: 51px;
|
||||
height: 51px;
|
||||
}
|
||||
.PixelPaw002 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -473px -324px;
|
||||
width: 51px;
|
||||
height: 51px;
|
||||
}
|
||||
.empty_bottles {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1620px -1583px;
|
||||
width: 64px;
|
||||
height: 54px;
|
||||
}
|
||||
.ghost {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1627px -1444px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.inventory_present {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -1233px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_01 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -324px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_02 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -435px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_03 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -504px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_04 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -573px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_05 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -655px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_06 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -724px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_07 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -793px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_08 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -875px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_09 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -944px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_10 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -1013px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_11 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -1095px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_present_12 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -1164px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_birthday {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -309px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_congrats {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -378px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_fortify {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -447px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_getwell {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -516px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_goodluck {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -585px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_greeting {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -654px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_nye {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -723px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_opaquePotion {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -792px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_seafoam {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -861px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_shinySeed {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -930px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_snowball {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -999px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_spookySparkles {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1068px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_thankyou {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1137px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_trinket {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1206px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_special_valentine {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1275px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.knockout {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -309px -1535px;
|
||||
width: 120px;
|
||||
height: 47px;
|
||||
}
|
||||
.pet_key {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1413px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.rebirth_orb {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1482px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.seafoam_star {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1718px -1444px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.shop_armoire {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1551px -1583px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.zzz {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -702px -261px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.zzz_light {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -702px -220px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.notif_inventory_present_01 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1809px -1444px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_02 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1809px -1473px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_03 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1809px -1502px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_04 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1187px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_05 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1216px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_06 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1245px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_07 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1274px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_08 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1303px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_09 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1332px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_10 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1361px -1061px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_11 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -967px -838px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_present_12 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -996px -838px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.notif_inventory_special_birthday {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1102px -838px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
}
|
||||
.notif_inventory_special_congrats {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1165px -838px;
|
||||
width: 20px;
|
||||
height: 22px;
|
||||
}
|
||||
.notif_inventory_special_getwell {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -747px -618px;
|
||||
width: 20px;
|
||||
height: 22px;
|
||||
}
|
||||
.notif_inventory_special_goodluck {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1081px -838px;
|
||||
width: 20px;
|
||||
height: 26px;
|
||||
}
|
||||
.notif_inventory_special_greeting {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -768px -618px;
|
||||
width: 20px;
|
||||
height: 22px;
|
||||
}
|
||||
.notif_inventory_special_nye {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1056px -838px;
|
||||
width: 24px;
|
||||
height: 26px;
|
||||
}
|
||||
.notif_inventory_special_thankyou {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1123px -838px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
}
|
||||
.notif_inventory_special_valentine {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1144px -838px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
}
|
||||
.npc_bailey {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -302px -1677px;
|
||||
width: 60px;
|
||||
height: 72px;
|
||||
}
|
||||
.npc_justin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -203px;
|
||||
width: 84px;
|
||||
height: 120px;
|
||||
}
|
||||
.npc_matt {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1375px -1315px;
|
||||
width: 195px;
|
||||
height: 138px;
|
||||
}
|
||||
.background_dysheartener {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px 0px;
|
||||
width: 306px;
|
||||
height: 202px;
|
||||
}
|
||||
.banner_flair_dysheartener {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px -856px;
|
||||
background-position: -1407px -1287px;
|
||||
width: 69px;
|
||||
height: 18px;
|
||||
}
|
||||
.phobia_dysheartener {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1510px;
|
||||
background-position: -1627px -1061px;
|
||||
width: 201px;
|
||||
height: 195px;
|
||||
}
|
||||
.quest_alligator {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -1079px;
|
||||
background-position: -1627px -645px;
|
||||
width: 201px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_amber {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -660px;
|
||||
background-position: -307px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_armadillo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -672px;
|
||||
background-position: -527px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_atom1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -657px -1332px;
|
||||
background-position: -665px -1315px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_atom2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1159px -1332px;
|
||||
background-position: -1167px -1315px;
|
||||
width: 207px;
|
||||
height: 138px;
|
||||
}
|
||||
.quest_atom3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -413px -1510px;
|
||||
background-position: -1187px -880px;
|
||||
width: 216px;
|
||||
height: 180px;
|
||||
}
|
||||
.quest_axolotl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -232px;
|
||||
background-position: 0px -435px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_badger {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px 0px;
|
||||
background-position: -220px -435px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_basilist {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -844px -1510px;
|
||||
background-position: 0px -1535px;
|
||||
width: 189px;
|
||||
height: 141px;
|
||||
}
|
||||
.quest_beetle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1102px -1112px;
|
||||
background-position: -1627px -859px;
|
||||
width: 204px;
|
||||
height: 201px;
|
||||
}
|
||||
.quest_bronze {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -452px;
|
||||
background-position: -440px -435px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_bunny {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -202px -1510px;
|
||||
background-position: -1627px -1257px;
|
||||
width: 210px;
|
||||
height: 186px;
|
||||
}
|
||||
.quest_butterfly {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -452px;
|
||||
background-position: -747px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_cheetah {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px 0px;
|
||||
background-position: -747px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_cow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px 0px;
|
||||
background-position: -527px -220px;
|
||||
width: 174px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_dilatory {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -440px;
|
||||
background-position: -220px -655px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dilatoryDistress1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -868px;
|
||||
background-position: -1627px -434px;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
}
|
||||
.quest_dilatoryDistress2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px -573px;
|
||||
background-position: 0px -1677px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_dilatoryDistress3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -672px;
|
||||
background-position: -440px -655px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dilatory_derby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -220px;
|
||||
background-position: 0px -655px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -672px;
|
||||
background-position: -660px -655px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dustbunnies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px 0px;
|
||||
background-position: -967px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_egg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px -214px;
|
||||
background-position: -307px -220px;
|
||||
width: 165px;
|
||||
height: 207px;
|
||||
}
|
||||
.quest_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px -724px;
|
||||
background-position: -190px -1535px;
|
||||
width: 118px;
|
||||
height: 131px;
|
||||
}
|
||||
.quest_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -440px;
|
||||
background-position: -967px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px 0px;
|
||||
background-position: -967px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_ferret {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -892px;
|
||||
background-position: 0px -875px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_fluorite {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -892px;
|
||||
background-position: -220px -875px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_frog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -1112px;
|
||||
background-position: -220px -1315px;
|
||||
width: 221px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_ghost_stag {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -892px;
|
||||
background-position: -440px -875px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -892px;
|
||||
background-position: -660px -875px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -908px -1332px;
|
||||
background-position: -916px -1315px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_goldenknight3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px 0px;
|
||||
background-position: 0px -203px;
|
||||
width: 219px;
|
||||
height: 231px;
|
||||
}
|
||||
.quest_gryphon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -1332px;
|
||||
background-position: -967px -660px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_guineapig {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -440px;
|
||||
background-position: -880px -875px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_harpy {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -660px;
|
||||
background-position: -1187px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_hedgehog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1307px -1112px;
|
||||
background-position: -1407px -1100px;
|
||||
width: 219px;
|
||||
height: 186px;
|
||||
}
|
||||
.quest_hippo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1112px;
|
||||
background-position: -1187px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_horse {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -1112px;
|
||||
background-position: -1187px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kangaroo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -1112px;
|
||||
background-position: -1187px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kraken {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -223px -1332px;
|
||||
background-position: -747px -440px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_lostMasterclasser1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -880px;
|
||||
background-position: 0px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -892px;
|
||||
background-position: -220px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -220px;
|
||||
background-position: -440px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1757px -422px;
|
||||
background-position: -151px -1677px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_mayhemMistiflying2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -452px;
|
||||
background-position: -660px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -232px;
|
||||
background-position: -880px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_monkey {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px 0px;
|
||||
background-position: -1100px -1095px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -651px;
|
||||
background-position: -1627px 0px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_moon2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -1112px;
|
||||
background-position: -1407px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -220px;
|
||||
background-position: -1407px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -892px;
|
||||
background-position: -1407px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -452px;
|
||||
background-position: -1407px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -232px;
|
||||
background-position: -1407px -880px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_nudibranch {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -434px;
|
||||
background-position: -1627px -217px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_octopus {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1332px;
|
||||
background-position: -442px -1315px;
|
||||
width: 222px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_owl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -672px;
|
||||
background-position: 0px -1315px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_peacock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px 0px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_penguin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1706px;
|
||||
width: 190px;
|
||||
height: 183px;
|
||||
}
|
||||
.quest_pterodactyl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rat {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_robot {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -217px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_rooster {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -630px -1510px;
|
||||
width: 213px;
|
||||
height: 174px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 472 KiB |
|
Before Width: | Height: | Size: 592 KiB After Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 114 KiB |