Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e20ee4aa | ||
|
|
a751a367fc | ||
|
|
d323be19c6 | ||
|
|
be3f61a94b | ||
|
|
f1bb2db73b | ||
|
|
a622344d44 | ||
|
|
e279a3550b | ||
|
|
70aab3059c | ||
|
|
c264e37182 | ||
|
|
b31bc15493 | ||
|
|
ba19c00617 | ||
|
|
93aa92de7c | ||
|
|
d021680945 | ||
|
|
f9595af8a5 | ||
|
|
d2756278c3 | ||
|
|
2e2dc179c4 | ||
|
|
acf7b811ab | ||
|
|
d5170251c0 | ||
|
|
c9ba9054e3 | ||
|
|
d4aac1ee4b | ||
|
|
9615a332a5 | ||
|
|
417455e5ef | ||
|
|
136502a110 | ||
|
|
425887c1e4 | ||
|
|
cfa8a5190f | ||
|
|
df5be81706 | ||
|
|
08b3491047 | ||
|
|
e73c3147c1 | ||
|
|
a43254000e | ||
|
|
4e3c984baf | ||
|
|
c112e923f1 | ||
|
|
540353f024 | ||
|
|
2b9b5e369e | ||
|
|
cb38475765 | ||
|
|
8bb92577b0 | ||
|
|
fb26cbd26d | ||
|
|
a0de5cd8f8 | ||
|
|
9fe10b1818 | ||
|
|
d8dd39422a | ||
|
|
3f9b710773 | ||
|
|
8a8bab4be1 | ||
|
|
2a0747ed72 | ||
|
|
a5196e94f6 | ||
|
|
009ab26711 | ||
|
|
3fabf3391f | ||
|
|
8020990264 | ||
|
|
a2cfeafc02 | ||
|
|
d04a4fb1ed | ||
|
|
aeb86db306 | ||
|
|
49960c0e32 | ||
|
|
932cb5cf6a | ||
|
|
74d6e77504 | ||
|
|
8400f1786b | ||
|
|
d7bd5dd9f8 | ||
|
|
3288b0de33 | ||
|
|
c025ffbd10 | ||
|
|
afb5b473a3 | ||
|
|
aeee29f5fa | ||
|
|
0cca2a07a2 | ||
|
|
55d94c129a | ||
|
|
358e1aed22 | ||
|
|
36241f061f | ||
|
|
b6201a3b75 | ||
|
|
005f74d918 | ||
|
|
926e188017 | ||
|
|
94da808279 | ||
|
|
7568dd52e9 | ||
|
|
c6e2b78982 | ||
|
|
b6104c3ef3 | ||
|
|
56b5c960f0 | ||
|
|
528abf77af | ||
|
|
8db6b7c6cb | ||
|
|
578dee59bd | ||
|
|
d40c923e6e | ||
|
|
3c4c64b023 | ||
|
|
c84d6ba141 | ||
|
|
5f3b147d2a | ||
|
|
ff08e8b586 | ||
|
|
cb2acbfefd | ||
|
|
b16da35585 | ||
|
|
826d7b85d7 | ||
|
|
6bcc6a15e2 | ||
|
|
b600eceb49 | ||
|
|
b83ef872c9 | ||
|
|
4ebc2e2175 | ||
|
|
2f4b8c569a | ||
|
|
85b5b5a62d | ||
|
|
e271e57f63 | ||
|
|
558fb145b5 | ||
|
|
fc30456b53 | ||
|
|
68b2d19b04 | ||
|
|
6d33acccf4 | ||
|
|
acee4bad80 | ||
|
|
30fe5088b8 | ||
|
|
69602f93e9 | ||
|
|
0109aa4250 | ||
|
|
2dc0958678 | ||
|
|
52f4e5f37d | ||
|
|
c014da297c | ||
|
|
285041cdee | ||
|
|
6a82206f81 | ||
|
|
8b6052a3ca | ||
|
|
04fd907a45 | ||
|
|
70343079f1 | ||
|
|
df952eece5 | ||
|
|
e3a619c7ff | ||
|
|
23f531372b | ||
|
|
97b15006fd | ||
|
|
35b92f13a3 | ||
|
|
556a7e5229 | ||
|
|
378625b4af | ||
|
|
ee15e29ba4 | ||
|
|
ed880a665a | ||
|
|
3c7f71d214 | ||
|
|
edac06b0d1 | ||
|
|
24562f8d60 | ||
|
|
97840ed732 | ||
|
|
76499412ed | ||
|
|
9b10f348cc | ||
|
|
17b0329c43 | ||
|
|
cda84a6d68 | ||
|
|
306505ebab | ||
|
|
2476cdd873 | ||
|
|
8465dd69be | ||
|
|
461e7445c2 | ||
|
|
24df8d8f2f | ||
|
|
2bca92b4d5 | ||
|
|
c3843cae80 | ||
|
|
816e4a2f19 | ||
|
|
d0d4927e59 | ||
|
|
023ff5789d | ||
|
|
cc9be6f4a1 | ||
|
|
145bcb6f7c | ||
|
|
d7db599f88 | ||
|
|
ca935670f7 | ||
|
|
c2eb113672 | ||
|
|
257e932bc3 | ||
|
|
50e2731811 | ||
|
|
d67b9e5688 | ||
|
|
bfc7b9d3e8 | ||
|
|
eb0e234afa | ||
|
|
177f78cbb0 | ||
|
|
e3b484b29a |
@@ -10,7 +10,6 @@ dist-client/
|
||||
# Not linted
|
||||
migrations/*
|
||||
website/client-old/
|
||||
debug-scripts/*
|
||||
scripts/*
|
||||
test/server_side/**/*
|
||||
test/client-old/spec/**/*
|
||||
@@ -23,4 +22,6 @@ Gruntfile.js
|
||||
gulpfile.js
|
||||
gulp
|
||||
webpack
|
||||
test/client
|
||||
test/client/e2e
|
||||
test/client/unit/index.js
|
||||
test/client/unit/karma.conf.js
|
||||
|
||||
1
.gitignore
vendored
@@ -14,7 +14,6 @@ npm-debug.log*
|
||||
lib
|
||||
website/client-old/bower_components
|
||||
website/client-old/new-stuff.html
|
||||
website/build
|
||||
newrelic_agent.log
|
||||
.bower-tmp
|
||||
.bower-registry
|
||||
|
||||
25
.travis.yml
@@ -2,16 +2,21 @@ language: node_js
|
||||
node_js:
|
||||
- '4.3.1'
|
||||
before_install:
|
||||
- "npm install -g npm@3"
|
||||
- "npm install -g gulp"
|
||||
- "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10"
|
||||
- "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list"
|
||||
- "sudo apt-get update"
|
||||
- "sudo apt-get install mongodb-org-server"
|
||||
- npm install -g npm@3
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
before_script:
|
||||
- 'npm install -g grunt-cli mocha'
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
- "until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done"
|
||||
- "export DISPLAY=:99"
|
||||
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
|
||||
after_script:
|
||||
- "./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js"
|
||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
||||
script: npm run $TEST
|
||||
env:
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content"
|
||||
- TEST="test:common"
|
||||
- TEST="test:karma"
|
||||
- TEST="client:unit"
|
||||
|
||||
10
Gruntfile.js
@@ -126,15 +126,7 @@ module.exports = function(grunt) {
|
||||
// Register tasks.
|
||||
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
|
||||
grunt.registerTask('build:dev', ['cssmin', 'stylus']);
|
||||
grunt.registerTask('build:test', ['test:prepare:translations', 'build:dev']);
|
||||
|
||||
grunt.registerTask('test:prepare:translations', function() {
|
||||
var i18n = require('./website/server/libs/i18n'),
|
||||
fs = require('fs');
|
||||
fs.writeFileSync('test/client-old/spec/mocks/translations.js',
|
||||
"if(!window.env) window.env = {};\n" +
|
||||
"window.env.translations = " + JSON.stringify(i18n.translations['en']) + ';');
|
||||
});
|
||||
grunt.registerTask('build:test', ['build:dev']);
|
||||
|
||||
// Load tasks
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
|
||||
20
README.md
@@ -1,4 +1,4 @@
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitrpg) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitica) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
===============
|
||||
|
||||
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
|
||||
@@ -10,21 +10,3 @@ For an introduction to the technologies used and how the software is organized,
|
||||
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
|
||||
|
||||
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
|
||||
|
||||
## Debug Scripts
|
||||
|
||||
In the `./debug-scripts/` folder, there are a few files. Here's a sample:
|
||||
|
||||
```bash
|
||||
grant-all-equipment.js
|
||||
grant-all-mounts.js
|
||||
grant-all-pets.js
|
||||
```
|
||||
|
||||
You can run them by doing:
|
||||
|
||||
```bash
|
||||
node debug-scripts/name-of-script.js
|
||||
```
|
||||
|
||||
If there are more arguments required to make the script work, it will print out the usage and an explanation of what the script does.
|
||||
|
||||
@@ -36,14 +36,15 @@
|
||||
"jquery-ui": "1.10.3",
|
||||
"jquery.cookie": "1.4.0",
|
||||
"js-emoji": "snicker/js-emoji#f25d8a303f",
|
||||
"ngInfiniteScroll": "1.0.0",
|
||||
"ngInfiniteScroll": "1.1.0",
|
||||
"pnotify": "1.3.1",
|
||||
"sticky": "1.0.3",
|
||||
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
|
||||
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
|
||||
"habitica-markdown": "1.2.2",
|
||||
"pusher-js-auth": "^2.0.0",
|
||||
"pusher-websocket-iso": "pusher#^3.2.0"
|
||||
"pusher-websocket-iso": "pusher#^3.2.0",
|
||||
"taggle": "^1.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "1.3.9"
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"FACEBOOK_ANALYTICS":"1234567890123456",
|
||||
"FACEBOOK_KEY":"123456789012345",
|
||||
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"GOOGLE_CLIENT_ID":"123456789012345",
|
||||
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"NODE_DB_URI":"mongodb://localhost/habitrpg",
|
||||
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
|
||||
"NODE_ENV":"development",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MongoClient as mongo } from 'mongodb';
|
||||
import config from '../config';
|
||||
|
||||
module.exports.updateUser = (_id, path, value) => {
|
||||
mongo.connect(config.NODE_DB_URI, (err, db) => {
|
||||
if (err) throw err;
|
||||
|
||||
let collection = db.collection('users');
|
||||
collection.updateOne(
|
||||
{ _id },
|
||||
{ $set: { [`${path}`]: value } },
|
||||
(updateErr, result) => {
|
||||
if (updateErr) throw updateErr;
|
||||
console.log('done updating', _id);
|
||||
db.close();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-equipment.js <user_id>');
|
||||
console.error('EFFECT: Adds all gear to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let gearFlat = require('../common').content.gear.flat;
|
||||
|
||||
let userGear = {};
|
||||
|
||||
_.each(gearFlat, (piece, key) => {
|
||||
userGear[key] = true;
|
||||
});
|
||||
|
||||
updateUser(userId, 'items.gear.owned', userGear);
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-mounts.js <user_id>');
|
||||
console.error('EFFECT: Adds all mounts to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let dropMounts = require('../common').content.mounts;
|
||||
let questMounts = require('../common').content.questMounts;
|
||||
let specialMounts = require('../common').content.specialMounts;
|
||||
let premiumMounts = require('../common').content.premiumPets; // premium mounts isn't exposed on the content object
|
||||
|
||||
let userMounts = {};
|
||||
|
||||
_.each([ dropMounts, questMounts, specialMounts, premiumMounts ], (set) => {
|
||||
_.each(set, (pet, key) => {
|
||||
userMounts[key] = true;
|
||||
});
|
||||
})
|
||||
|
||||
updateUser(userId, 'items.mounts', userMounts);
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-pets.js <user_id>');
|
||||
console.error('EFFECT: Adds all pets to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let dropPets = require('../common').content.pets;
|
||||
let questPets = require('../common').content.questPets;
|
||||
let specialPets = require('../common').content.specialPets;
|
||||
let premiumPets = require('../common').content.premiumPets;
|
||||
|
||||
let userPets = {};
|
||||
|
||||
_.each([ dropPets, questPets, specialPets, premiumPets ], (set) => {
|
||||
_.each(set, (pet, key) => {
|
||||
userPets[key] = 95;
|
||||
});
|
||||
})
|
||||
|
||||
updateUser(userId, 'items.pets', userPets);
|
||||
@@ -20,3 +20,7 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('apidoc:watch', ['apidoc'], () => {
|
||||
return gulp.watch(APIDOC_SRC_PATH + '/**/*.js', ['apidoc']);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
|
||||
|
||||
// Relative to node_modules/semantic-ui-less
|
||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/semantic-ui/theme.config';
|
||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
|
||||
|
||||
// fix well known bug with default distribution
|
||||
function fixFontPath (filename) {
|
||||
|
||||
@@ -13,6 +13,9 @@ import Bluebird from 'bluebird';
|
||||
import runSequence from 'run-sequence';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import fs from 'fs';
|
||||
|
||||
const i18n = require('../website/server/libs/i18n');
|
||||
|
||||
// TODO rewrite
|
||||
|
||||
@@ -72,10 +75,17 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('test:prepare:build', ['build'], (cb) => {
|
||||
exec(testBin('grunt build:test'), cb);
|
||||
gulp.task('test:prepare:translations', (cb) => {
|
||||
fs.writeFile(
|
||||
'test/client-old/spec/mocks/translations.js',
|
||||
`if(!window.env) window.env = {};
|
||||
window.env.translations = ${JSON.stringify(i18n.translations['en'])};`, cb);
|
||||
|
||||
});
|
||||
|
||||
gulp.task('test:prepare:build', ['build', 'test:prepare:translations']);
|
||||
// exec(testBin('grunt build:test'), cb);
|
||||
|
||||
gulp.task('test:prepare:webdriver', (cb) => {
|
||||
exec('npm run test:prepare:webdriver', cb);
|
||||
});
|
||||
@@ -175,32 +185,6 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:server_side', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SERVER_SIDE_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:server_side:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SERVER_SIDE_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
testResults.push({
|
||||
suite: 'Server Side Specs',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
fail: testCount(stdout, /(\d+) failing/),
|
||||
pend: testCount(stdout, /(\d+) pending/),
|
||||
});
|
||||
cb();
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:karma', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(KARMA_TEST_COMMAND),
|
||||
@@ -296,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
|
||||
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/unit --recursive'),
|
||||
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
@@ -314,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
|
||||
|
||||
gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive'),
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
|
||||
116
migrations/20161002_add_missing_webhook_type.js
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
/****************************************
|
||||
* Author: Blade Barringer @crookedneighbor
|
||||
*
|
||||
* Reason: Webhooks have been moved from
|
||||
* being an object on preferences.webhooks
|
||||
* to being an array on webhooks. In addition
|
||||
* they support a type and options and label
|
||||
* ***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
const connectToDb = require('./utils/connect').connectToDb;
|
||||
const closeDb = require('./utils/connect').closeDb;
|
||||
const validator = require('validator');
|
||||
|
||||
const timer = new Timer();
|
||||
const MIGRATION_NAME = '20161002_add_missing_webhook_type.js';
|
||||
|
||||
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
|
||||
const DB_URI = 'mongodb://localhost/prod-copy-1';
|
||||
|
||||
const LOGGEDIN_DATE_RANGE = {
|
||||
$gte: new Date("2016-09-30T00:00:00.000Z"),
|
||||
// $lte: new Date("2016-09-25T00:00:00.000Z"),
|
||||
};
|
||||
|
||||
let Users;
|
||||
|
||||
connectToDb(DB_URI).then((db) => {
|
||||
Users = db.collection('users');
|
||||
})
|
||||
.then(findUsersWithWebhooks)
|
||||
.then(correctWebhooks)
|
||||
.then(() => {
|
||||
timer.stop();
|
||||
closeDb();
|
||||
}).catch(reportError);
|
||||
|
||||
function reportError (err) {
|
||||
logger.error('Uh oh, an error occurred');
|
||||
logger.error(err);
|
||||
closeDb();
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
// Cached ids of users that need updating
|
||||
const USER_IDS = require('../../ids_of_webhooks_to_update.json');
|
||||
|
||||
function findUsersWithWebhooks () {
|
||||
logger.warn('Fetching users with webhooks...');
|
||||
|
||||
return Users.find({'_id': {$in: USER_IDS}}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
// return Users.find({'preferences.webhooks': {$ne: {} }}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
// TODO: Run this after the initial migration to catch any webhooks that may have been aded since the prod backup download
|
||||
// return Users.find({'preferences.webhooks': {$ne: {} }, 'auth.timestamps.loggedin': LOGGEDIN_DATE_RANGE}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
let updates = docs.map((user) => {
|
||||
let oldWebhooks = user.preferences.webhooks;
|
||||
let webhooks = Object.keys(oldWebhooks).map((id) => {
|
||||
let webhook = oldWebhooks[id]
|
||||
|
||||
webhook.type = 'taskActivity';
|
||||
webhook.label = '';
|
||||
webhook.options = {
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
};
|
||||
|
||||
return webhook;
|
||||
}).sort((a, b) => {
|
||||
return a.sort - b.sort;
|
||||
});
|
||||
|
||||
return {
|
||||
webhooks,
|
||||
id: user._id,
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(updates);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserById (user) {
|
||||
let userId = user.id;
|
||||
let webhooks = user.webhooks;
|
||||
|
||||
return Users.findOneAndUpdate({
|
||||
_id: userId},
|
||||
{$set: {webhooks: webhooks, migration: MIGRATION_NAME}
|
||||
}, {returnOriginal: false})
|
||||
}
|
||||
|
||||
function correctWebhooks (users) {
|
||||
let queue = new TaskQueue(Promise, 300);
|
||||
|
||||
logger.warn('About to update', users.length, 'users...');
|
||||
|
||||
return Promise.map(users, queue.wrap(updateUserById)).then((result) => {
|
||||
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
|
||||
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
|
||||
|
||||
logger.warn(updates.length, 'users have been fixed');
|
||||
|
||||
if (failures.length > 0) {
|
||||
logger.error(failures.length, 'users could not be found');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
73
migrations/20161002_takeThis.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var migrationName = '20161002_takeThis.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award Take This ladder items to participants in this month's challenge
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['4bbf63b5-10bc-49f9-8e95-5bd2ac99cd1c']}
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['head_mystery_201609','armor_mystery_201609']
|
||||
$each:['head_mystery_201610','armor_mystery_201610']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ function connectToDb (dbUri) {
|
||||
function closeDb () {
|
||||
if (db) db.close();
|
||||
|
||||
logger.success('CLosed connection to the database');
|
||||
logger.success('Closed connection to the database');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
1041
npm-shrinkwrap.json
generated
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.43.2",
|
||||
"version": "3.51.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
@@ -16,6 +16,7 @@
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-polyfill": "^6.6.1",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-register": "^6.6.0",
|
||||
@@ -34,7 +35,7 @@
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"estraverse": "^4.1.1",
|
||||
"express": "~4.13.3",
|
||||
"express": "~4.14.0",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
@@ -69,6 +70,7 @@
|
||||
"less-loader": "^2.2.3",
|
||||
"lodash": "^3.10.1",
|
||||
"lodash.setwith": "^4.2.0",
|
||||
"lodash.pickby": "^4.2.0",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
@@ -85,6 +87,7 @@
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "~0.2.1",
|
||||
"passport-facebook": "2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.2.1",
|
||||
"pretty-data": "^0.40.0",
|
||||
@@ -92,7 +95,7 @@
|
||||
"pug": "^2.0.0-beta6",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.72.0",
|
||||
"request": "~2.74.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"run-sequence": "^1.1.4",
|
||||
"s3-upload-stream": "^1.0.6",
|
||||
@@ -113,8 +116,6 @@
|
||||
"vue-loader": "^9.4.0",
|
||||
"vue-resource": "^1.0.2",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vuex": "^2.0.0-rc.5",
|
||||
"vuex-router-sync": "^3.0.0",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-merge": "^0.8.3",
|
||||
"winston": "^2.1.0",
|
||||
@@ -128,6 +129,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
"test:api-v3:integration": "gulp test:api-v3:integration",
|
||||
@@ -146,6 +148,7 @@
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "node webpack/build.js",
|
||||
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "karma start test/client/unit/karma.conf.js",
|
||||
"client:e2e": "node test/client/e2e/runner.js",
|
||||
"client:test": "npm run client:unit && npm run client:e2e",
|
||||
"start": "gulp run:dev",
|
||||
@@ -202,7 +205,6 @@
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-source-stream": "^1.0.0",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-dev-middleware": "^1.4.0",
|
||||
"webpack-hot-middleware": "^2.6.0"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
sleep,
|
||||
server,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
let user, groupWithChat, userWithChatRevoked, member;
|
||||
@@ -40,7 +43,7 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when chat privileges are revoked', async () => {
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
@@ -48,12 +51,86 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Private Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let privateGuildMemberWithChatsRevoked = members[0];
|
||||
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||
|
||||
let message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a message to a party with a user with revoked chat', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let privatePartyMemberWithChatsRevoked = members[0];
|
||||
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||
|
||||
let message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat', async () => {
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('sends group chat received webhooks', async () => {
|
||||
let userUuid = generateUUID();
|
||||
let memberUuid = generateUUID();
|
||||
await server.start();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${userUuid}`,
|
||||
type: 'groupChatReceived',
|
||||
enabled: true,
|
||||
options: {
|
||||
groupId: groupWithChat.id,
|
||||
},
|
||||
});
|
||||
await member.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${memberUuid}`,
|
||||
type: 'groupChatReceived',
|
||||
enabled: true,
|
||||
options: {
|
||||
groupId: groupWithChat.id,
|
||||
},
|
||||
});
|
||||
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
await sleep();
|
||||
|
||||
await server.close();
|
||||
|
||||
let userBody = server.getWebhookData(userUuid);
|
||||
let memberBody = server.getWebhookData(memberUuid);
|
||||
|
||||
[userBody, memberBody].forEach((body) => {
|
||||
expect(body.group.id).to.eql(groupWithChat._id);
|
||||
expect(body.group.name).to.eql(groupWithChat.name);
|
||||
expect(body.chat).to.eql(message.message);
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies other users of new messages for a guild', async () => {
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
let memberWithNotification = await member.get('/user');
|
||||
|
||||
@@ -29,14 +29,6 @@ describe('POST /coupons/generate/:event', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if event is missing', async () => {
|
||||
await expect(user.post('/coupons/generate')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if event is invalid', async () => {
|
||||
await expect(user.post('/coupons/generate/notValid?count=1')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
|
||||
@@ -65,6 +65,19 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
expect(groupToLeave.leader).to.equal(member._id);
|
||||
});
|
||||
|
||||
it('removes new messages for that group from user', async () => {
|
||||
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
|
||||
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
|
||||
|
||||
await leader.post(`/groups/${groupToLeave._id}/leave`);
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
|
||||
});
|
||||
|
||||
context('With challenges', () => {
|
||||
let challenge;
|
||||
|
||||
@@ -122,6 +135,8 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
privateGuild = group;
|
||||
leader = groupLeader;
|
||||
invitedUser = invitees[0];
|
||||
|
||||
await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
|
||||
});
|
||||
|
||||
it('removes a group when the last member leaves', async () => {
|
||||
|
||||
@@ -87,6 +87,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
let partyLeader;
|
||||
let partyInvitedUser;
|
||||
let partyMember;
|
||||
let removedMember;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
|
||||
@@ -96,13 +97,14 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
privacy: 'private',
|
||||
},
|
||||
invites: 1,
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
party = group;
|
||||
partyLeader = groupLeader;
|
||||
partyInvitedUser = invitees[0];
|
||||
partyMember = members[0];
|
||||
removedMember = members[1];
|
||||
});
|
||||
|
||||
it('can remove other members', async () => {
|
||||
@@ -129,6 +131,18 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
expect(invitedUserWithoutInvite.invitations.party).to.be.empty;
|
||||
});
|
||||
|
||||
it('removes new messages from a member who is removed', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.newMessages[party._id]).to.not.be.empty;
|
||||
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.newMessages[party._id]).to.be.empty;
|
||||
});
|
||||
|
||||
it('removes user from quest when removing user from party after quest starts', async () => {
|
||||
let petQuest = 'whale';
|
||||
await partyLeader.update({
|
||||
|
||||
@@ -57,11 +57,27 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty when uuids is empty', async () => {
|
||||
it('returns an error when uuids and emails are empty', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [],
|
||||
uuids: [],
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMustNotBeEmpty'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when uuids is empty and emails is not passed', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [],
|
||||
}))
|
||||
.to.eventually.be.empty;
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingUuid'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when there are more than INVITES_LIMIT uuids', async () => {
|
||||
@@ -159,11 +175,15 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty when emails is an empty array', async () => {
|
||||
it('returns an error when emails is empty and uuids is not passed', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [],
|
||||
}))
|
||||
.to.eventually.be.empty;
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingEmail'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when there are more than INVITES_LIMIT emails', async () => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
generateGroup,
|
||||
sleep,
|
||||
generateChallenge,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
let user;
|
||||
@@ -42,6 +47,77 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks if task is user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
|
||||
await user.del(`/tasks/${task.id}`);
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.type).to.eql('deleted');
|
||||
expect(body.task).to.eql(task);
|
||||
});
|
||||
|
||||
it('does not send task activity webhooks if task is not user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.update({
|
||||
balance: 10,
|
||||
});
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
|
||||
let challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.del(`/tasks/${challengeTask.id}`);
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('task cannot be deleted', () => {
|
||||
it('cannot delete a non-existant task', async () => {
|
||||
await expect(user.del('/tasks/550e8400-e29b-41d4-a716-446655440000')).to.eventually.be.rejected.and.eql({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
translate as t,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
@@ -45,6 +47,40 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends task scored webhooks', async () => {
|
||||
let uuid = generateUUID();
|
||||
await server.start();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
scored: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task.id}/score/up`);
|
||||
|
||||
await sleep();
|
||||
|
||||
await server.close();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.user).to.have.all.keys('_id', '_tmp', 'stats');
|
||||
expect(body.user.stats).to.have.all.keys('hp', 'mp', 'exp', 'gp', 'lvl', 'class', 'points', 'str', 'con', 'int', 'per', 'buffs', 'training', 'maxHealth', 'maxMP', 'toNextLevel');
|
||||
expect(body.task.id).to.eql(task.id);
|
||||
expect(body.direction).to.eql('up');
|
||||
expect(body.delta).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
translate as t,
|
||||
server,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/user', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
@@ -205,6 +207,71 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.task).to.eql(task);
|
||||
});
|
||||
|
||||
it('sends a task activity webhook for each task', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
|
||||
let tasks = await user.post('/tasks/user', [{
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
}, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
}]);
|
||||
|
||||
await sleep();
|
||||
|
||||
let taskBodies = [
|
||||
server.getWebhookData(uuid),
|
||||
server.getWebhookData(uuid),
|
||||
];
|
||||
|
||||
expect(taskBodies.find(body => body.task.id === tasks[0].id)).to.exist;
|
||||
expect(taskBodies.find(body => body.task.id === tasks[1].id)).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('all types', () => {
|
||||
it('can create reminders', async () => {
|
||||
let id1 = generateUUID();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
generateChallenge,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
@@ -145,6 +146,81 @@ describe('PUT /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks if task is user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
updated: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let updatedTask = await user.put(`/tasks/${task.id}`, {
|
||||
text: 'updated text',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.type).to.eql('updated');
|
||||
expect(body.task).to.eql(updatedTask);
|
||||
});
|
||||
|
||||
it('does not send task activity webhooks if task is not user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.update({
|
||||
balance: 10,
|
||||
});
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
updated: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.put(`/tasks/${task.id}`, {
|
||||
text: 'updated text',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('all types', () => {
|
||||
let daily;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('POST /tasks/:taskId', () => {
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -82,6 +82,17 @@ describe('POST /tasks/:taskId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows user to assign themselves', async () => {
|
||||
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('assigns a task to a user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE group /tasks/:taskId/checklist/:itemId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('deletes a checklist item', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {text: 'Checklist Item 1', completed: false});
|
||||
|
||||
await user.del(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`);
|
||||
savedTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(savedTask[0].checklist.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not work with habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not work with rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.del(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('checklistItemNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST group /tasks/:taskId/checklist/', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('adds a checklist item to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
ignored: false,
|
||||
_id: 123,
|
||||
});
|
||||
|
||||
let updatedTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
let updatedTask = updatedTasks[0];
|
||||
|
||||
expect(updatedTask.checklist.length).to.equal(1);
|
||||
expect(updatedTask.checklist[0].text).to.equal('Checklist Item 1');
|
||||
expect(updatedTask.checklist[0].completed).to.equal(false);
|
||||
expect(updatedTask.checklist[0].id).to.be.a('string');
|
||||
expect(updatedTask.checklist[0].id).to.not.equal('123');
|
||||
expect(updatedTask.checklist[0].ignored).to.be.an('undefined');
|
||||
});
|
||||
|
||||
it('does not add a checklist to habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${habit._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add a checklist to rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${reward._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('PUT group /tasks/:taskId/checklist/:itemId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('updates a checklist item', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
});
|
||||
|
||||
savedTask = await user.put(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`, {
|
||||
text: 'updated',
|
||||
completed: true,
|
||||
_id: 123, // ignored
|
||||
});
|
||||
|
||||
expect(savedTask.checklist.length).to.equal(1);
|
||||
expect(savedTask.checklist[0].text).to.equal('updated');
|
||||
expect(savedTask.checklist[0].completed).to.equal(true);
|
||||
expect(savedTask.checklist[0].id).to.not.equal('123');
|
||||
});
|
||||
|
||||
it('fails on habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.put(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('checklistItemNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
|
||||
xdescribe('DELETE group /tasks/:taskId/tags/:tagId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('removes a tag from a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
|
||||
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
await user.del(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
let updatedTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(updatedTask[0].tags.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('only deletes existing tags', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${createdTask._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('tagNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
|
||||
xdescribe('POST group /tasks/:taskId/tags/:tagId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('adds a tag to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
let savedTask = await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
expect(savedTask.tags[0]).to.equal(tag.id);
|
||||
});
|
||||
|
||||
it('does not add a tag to a task twice', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
|
||||
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/tags/${tag.id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('alreadyTagged'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add a non existing tag to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('DELETE /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('succeeds', async () => {
|
||||
let id = 'some-id';
|
||||
user.preferences.webhooks[id] = { url: 'http://some-url.com', enabled: true };
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
let response = await user.del(`${endpoint}/${id}`);
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
});
|
||||
});
|
||||
@@ -13,12 +13,19 @@ describe('GET /user/anonymized', () => {
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({ newMessages: ['some', 'new', 'messages'], 'profile.name': 'profile', 'purchased.plan': 'purchased plan',
|
||||
contributor: 'contributor', invitations: 'invitations', 'items.special.nyeReceived': 'some', 'items.special.valentineReceived': 'some',
|
||||
webhooks: 'some', 'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
});
|
||||
await user.update({
|
||||
newMessages: ['some', 'new', 'messages'],
|
||||
'profile.name': 'profile',
|
||||
'purchased.plan': 'purchased plan',
|
||||
contributor: 'contributor',
|
||||
invitations: 'invitations',
|
||||
'items.special.nyeReceived': 'some',
|
||||
'items.special.valentineReceived': 'some',
|
||||
webhooks: [{url: 'https://somurl.com'}],
|
||||
'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
});
|
||||
|
||||
await generateHabit({ userId: user._id });
|
||||
await generateHabit({ userId: user._id, text: generateUUID() });
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('POST /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates', async () => {
|
||||
await expect(user.post(endpoint, { enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidUrl'),
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
let response = await user.post(endpoint, { enabled: true, url: 'http://some-url.com'});
|
||||
expect(response.id).to.exist;
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.not.eql({});
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,7 @@ describe('PUT /user', () => {
|
||||
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
|
||||
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
|
||||
notifications: [{type: 123}],
|
||||
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let url = 'http://new-url.com';
|
||||
let enabled = true;
|
||||
|
||||
describe('PUT /user/webhook/:id', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validation fails', async () => {
|
||||
await expect(user.put('/user/webhook/some-id'), { enabled: true }).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidUrl'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', async () => {
|
||||
let response = await user.post('/user/webhook', { enabled: true, url: 'http://some-url.com'});
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks[response.id].url).to.not.eql(url);
|
||||
let response2 = await user.put(`/user/webhook/${response.id}`, {url, enabled});
|
||||
expect(response2.url).to.eql(url);
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks[response.id].url).to.eql(url);
|
||||
});
|
||||
});
|
||||
@@ -5,36 +5,94 @@ import {
|
||||
|
||||
describe('DELETE social registration', () => {
|
||||
let user;
|
||||
let endpoint = '/user/auth/social/facebook';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({ 'auth.facebook.id': 'some-fb-id' });
|
||||
expect(user.auth.local.username).to.not.be.empty;
|
||||
expect(user.auth.facebook).to.not.be.empty;
|
||||
});
|
||||
context('of NOT-FACEBOOK', () => {
|
||||
|
||||
context('NOT-SUPPORTED', () => {
|
||||
it('is not supported', async () => {
|
||||
await expect(user.del('/user/auth/social/SOME-OTHER-NETWORK')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyFbSupported'),
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('unsupportedNetwork'),
|
||||
});
|
||||
});
|
||||
});
|
||||
context('of facebook', () => {
|
||||
it('fails if local registration does not exist for this user', async () => {
|
||||
await user.update({ 'auth.local': { ok: true } });
|
||||
await expect(user.del(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
|
||||
context('Facebook', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachFb'),
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
let response = await user.del(endpoint);
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.empty;
|
||||
});
|
||||
|
||||
it('succeeds if user has a google registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
context('Google', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/google')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/google');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.google).to.be.empty;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/google');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.google).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,58 +12,132 @@ describe('POST /user/auth/social', () => {
|
||||
let endpoint = '/user/auth/social';
|
||||
let randomAccessToken = '123456';
|
||||
let facebookId = 'facebookId';
|
||||
let network = 'facebook';
|
||||
let googleId = 'googleId';
|
||||
let network = 'NoNetwork';
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
|
||||
let expectedResult = {id: facebookId};
|
||||
let passportFacebookProfile = sandbox.stub(passport._strategies.facebook, 'userProfile');
|
||||
passportFacebookProfile.yields(null, expectedResult);
|
||||
});
|
||||
|
||||
it('fails if network is not facebook', async () => {
|
||||
it('fails if network is not supported', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network: 'NotFacebook',
|
||||
network,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyFbSupported'),
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('unsupportedNetwork'),
|
||||
});
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
describe('facebook', () => {
|
||||
before(async () => {
|
||||
let expectedResult = {id: facebookId};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
network = 'facebook';
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
let registerResponse = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
let response = await user.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
describe('google', () => {
|
||||
before(async () => {
|
||||
let expectedResult = {id: googleId};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
network = 'google';
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
await user.update({ 'auth.facebook.id': facebookId });
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
it('logs an existing user in', async () => {
|
||||
let registerResponse = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
let response = await user.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user, webhookToDelete;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('DELETE /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
|
||||
webhookToDelete = await user.post('/user/webhook', {
|
||||
url: 'http://some-url.com',
|
||||
enabled: true,
|
||||
});
|
||||
await user.post('/user/webhook', {
|
||||
url: 'http://some-other-url.com',
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
});
|
||||
|
||||
it('deletes a webhook', async () => {
|
||||
expect(user.webhooks).to.have.a.lengthOf(2);
|
||||
await user.del(`${endpoint}/${webhookToDelete.id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.webhooks).to.have.a.lengthOf(1);
|
||||
});
|
||||
|
||||
it('returns the remaining webhooks', async () => {
|
||||
let [remainingWebhook] = await user.del(`${endpoint}/${webhookToDelete.id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks[0];
|
||||
|
||||
expect(remainingWebhook.id).to.eql(webhook.id);
|
||||
expect(remainingWebhook.url).to.eql(webhook.url);
|
||||
expect(remainingWebhook.type).to.eql(webhook.type);
|
||||
expect(remainingWebhook.options).to.eql(webhook.options);
|
||||
});
|
||||
|
||||
it('returns an error if webhook with id does not exist', async () => {
|
||||
await expect(user.del(`${endpoint}/id-that-does-not-exist`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
|
||||
});
|
||||
});
|
||||
});
|
||||
221
test/api/v3/integration/webhook/POST-user_add_webhook.test.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /user/webhook', () => {
|
||||
let user, body;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
body = {
|
||||
id: generateUUID(),
|
||||
url: 'https://example.com/endpoint',
|
||||
type: 'taskActivity',
|
||||
enabled: false,
|
||||
};
|
||||
});
|
||||
|
||||
it('requires a url', async () => {
|
||||
delete body.url;
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('requires custom id to be a uuid', async () => {
|
||||
body.id = 'not-a-uuid';
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults id to a uuid', async () => {
|
||||
delete body.id;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.id).to.exist;
|
||||
});
|
||||
|
||||
it('requires type to be of an accetable type', async () => {
|
||||
body.type = 'not a valid type';
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults enabled to true', async () => {
|
||||
delete body.enabled;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.enabled).to.be.true;
|
||||
});
|
||||
|
||||
it('can pass a label', async () => {
|
||||
body.label = 'Custom Label';
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.label).to.equal('Custom Label');
|
||||
});
|
||||
|
||||
it('defaults type to taskActivity', async () => {
|
||||
delete body.type;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.webhooks).to.eql([]);
|
||||
|
||||
let response = await user.post('/user/webhook', body);
|
||||
|
||||
expect(response.id).to.eql(body.id);
|
||||
expect(response.type).to.eql(body.type);
|
||||
expect(response.url).to.eql(body.url);
|
||||
expect(response.enabled).to.eql(body.enabled);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.webhooks).to.not.eql([]);
|
||||
|
||||
let webhook = user.webhooks[0];
|
||||
|
||||
expect(webhook.enabled).to.be.false;
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
expect(webhook.url).to.eql(body.url);
|
||||
});
|
||||
|
||||
it('cannot use an id of a webhook that already exists', async () => {
|
||||
await user.post('/user/webhook', body);
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookIdAlreadyTaken', { id: body.id }),
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can set taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('discards extra properties in taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options.foo).to.not.exist;
|
||||
expect(webhook.options).to.eql({
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
});
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
|
||||
it(`requires taskActivity option ${option} to be a boolean`, async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
[option]: 'not a boolean',
|
||||
};
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookBooleanOption', { option }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can set groupChatReceived options', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: generateUUID(),
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
groupId: body.options.groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('groupChatReceived options requires a uuid for the groupId', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: 'not-a-uuid',
|
||||
};
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupIdRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('discards extra properties in groupChatReceived options', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: generateUUID(),
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options.foo).to.not.exist;
|
||||
expect(webhook.options).to.eql({
|
||||
groupId: body.options.groupId,
|
||||
});
|
||||
});
|
||||
});
|
||||
132
test/api/v3/integration/webhook/PUT-user_update_webhook.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID} from 'uuid';
|
||||
|
||||
describe('PUT /user/webhook/:id', () => {
|
||||
let user, webhookToUpdate;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
|
||||
webhookToUpdate = await user.post('/user/webhook', {
|
||||
url: 'http://some-url.com',
|
||||
label: 'Original Label',
|
||||
enabled: true,
|
||||
type: 'taskActivity',
|
||||
options: { created: true, scored: true },
|
||||
});
|
||||
await user.post('/user/webhook', {
|
||||
url: 'http://some-other-url.com',
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
});
|
||||
|
||||
it('returns an error if webhook with id does not exist', async () => {
|
||||
await expect(user.put('/user/webhook/id-that-does-not-exist')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if validation fails', async () => {
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo', enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a webhook', async () => {
|
||||
let url = 'http://a-new-url.com';
|
||||
let type = 'groupChatReceived';
|
||||
let label = 'New Label';
|
||||
let options = { groupId: generateUUID() };
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options, label});
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.url).to.equal(url);
|
||||
expect(webhook.label).to.equal(label);
|
||||
expect(webhook.type).to.equal(type);
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('returns the updated webhook', async () => {
|
||||
let url = 'http://a-new-url.com';
|
||||
let type = 'groupChatReceived';
|
||||
let options = { groupId: generateUUID() };
|
||||
|
||||
let response = await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options});
|
||||
|
||||
expect(response.url).to.eql(url);
|
||||
expect(response.type).to.eql(type);
|
||||
expect(response.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('cannot update the id', async () => {
|
||||
let id = generateUUID();
|
||||
let url = 'http://a-new-url.com';
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, id});
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.id).to.eql(webhookToUpdate.id);
|
||||
expect(webhook.url).to.eql(url);
|
||||
});
|
||||
|
||||
it('can update taskActivity options', async () => {
|
||||
let type = 'taskActivity';
|
||||
let options = {
|
||||
updated: false,
|
||||
deleted: true,
|
||||
};
|
||||
|
||||
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: true, // starting value
|
||||
updated: false,
|
||||
deleted: true,
|
||||
scored: true, // default value
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if taskActivity option is not a boolean', async () => {
|
||||
let type = 'taskActivity';
|
||||
let options = {
|
||||
created: 'not a boolean',
|
||||
updated: false,
|
||||
deleted: true,
|
||||
};
|
||||
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookBooleanOption', { option: 'created' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if groupChatRecieved groupId option is not a uuid', async () => {
|
||||
let type = 'groupChatReceived';
|
||||
let options = {
|
||||
groupId: 'not-a-uuid',
|
||||
};
|
||||
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupIdRequired'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -277,6 +277,7 @@ describe('analyticsService', () => {
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}],
|
||||
balance: 12,
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
@@ -300,6 +301,7 @@ describe('analyticsService', () => {
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('cron', () => {
|
||||
describe('end of the month perks', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
@@ -71,10 +71,21 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets plan.dateUpdated on a new month', () => {
|
||||
let currentMonth = moment().format('MMYYYY');
|
||||
it('does not reset plan.gemsBought within the month', () => {
|
||||
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
||||
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('resets plan.dateUpdated on a new month', () => {
|
||||
let currentMonth = moment().startOf('month');
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.count', () => {
|
||||
@@ -83,6 +94,13 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.count by more than 1 if user skipped months between logins', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
|
||||
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
|
||||
user.purchased.plan.consecutive.offset = 2;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
@@ -97,6 +115,21 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
@@ -105,6 +138,13 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
@@ -118,7 +158,7 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
|
||||
it('does reset plan stats until we are after the last day of the cancelled month', () => {
|
||||
it('does reset plan stats if we are after the last day of the cancelled month', () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
|
||||
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
@@ -134,10 +174,25 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
describe('end of the month perks when user is not subscribed', () => {
|
||||
it('does not reset plan.gemsBought on a new month', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not reset plan.gemsBought within the month', () => {
|
||||
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
||||
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', () => {
|
||||
|
||||
@@ -80,6 +80,24 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
let dateTerminated = moment().subtract(2, 'months').toDate();
|
||||
recipient.purchased.plan.dateTerminated = dateTerminated;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.gemsBought = 12;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.gemsBought).to.eql(12);
|
||||
});
|
||||
|
||||
it('adds to date terminated for an existing plan with a future terminated date', async () => {
|
||||
let dateTerminated = moment().add(1, 'months').toDate();
|
||||
recipient.purchased.plan = plan;
|
||||
@@ -210,6 +228,25 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on additional subscription', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.gemsBought).to.eql(10);
|
||||
});
|
||||
|
||||
it('sets lastBillingDate if payment method is "Amazon Payments"', async () => {
|
||||
data.paymentMethod = 'Amazon Payments';
|
||||
|
||||
@@ -218,7 +255,7 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.lastBillingDate).to.exist;
|
||||
});
|
||||
|
||||
it('increases the user\'s transcation count', async () => {
|
||||
it('increases the user\'s transaction count', async () => {
|
||||
expect(user.purchased.txnCount).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -8,27 +8,30 @@ import nconf from 'nconf';
|
||||
|
||||
describe('slack', () => {
|
||||
describe('sendFlagNotification', () => {
|
||||
let flagger, group, message;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
flagger = {
|
||||
id: 'flagger-id',
|
||||
profile: {
|
||||
name: 'flagger',
|
||||
data = {
|
||||
authorEmail: 'author@example.com',
|
||||
flagger: {
|
||||
id: 'flagger-id',
|
||||
profile: {
|
||||
name: 'flagger',
|
||||
},
|
||||
},
|
||||
group: {
|
||||
id: 'group-id',
|
||||
privacy: 'private',
|
||||
name: 'Some group',
|
||||
type: 'guild',
|
||||
},
|
||||
message: {
|
||||
id: 'chat-id',
|
||||
user: 'Author',
|
||||
uuid: 'author-id',
|
||||
text: 'some text',
|
||||
},
|
||||
};
|
||||
group = {
|
||||
id: 'group-id',
|
||||
privacy: 'private',
|
||||
name: 'Some group',
|
||||
type: 'guild',
|
||||
};
|
||||
message = {
|
||||
id: 'chat-id',
|
||||
user: 'Author',
|
||||
uuid: 'author-id',
|
||||
text: 'some text',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -37,11 +40,7 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('sends a slack webhook', () => {
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
@@ -49,7 +48,7 @@ describe('slack', () => {
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: 'Author - author-id',
|
||||
author_name: 'Author - author@example.com - author-id',
|
||||
title: 'Flag in Some group - (private guild)',
|
||||
title_link: undefined,
|
||||
text: 'some text',
|
||||
@@ -62,13 +61,9 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('includes a title link if guild is public', () => {
|
||||
group.privacy = 'public';
|
||||
data.group.privacy = 'public';
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -79,15 +74,11 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('links to tavern', () => {
|
||||
group.privacy = 'public';
|
||||
group.name = 'Tavern';
|
||||
group.id = TAVERN_ID;
|
||||
data.group.privacy = 'public';
|
||||
data.group.name = 'Tavern';
|
||||
data.group.id = TAVERN_ID;
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -98,14 +89,10 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('provides name for system message', () => {
|
||||
message.uuid = 'system';
|
||||
delete message.user;
|
||||
data.message.uuid = 'system';
|
||||
delete data.message.user;
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -121,11 +108,7 @@ describe('slack', () => {
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
|
||||
reRequiredSlack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
reRequiredSlack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -1,135 +1,376 @@
|
||||
import request from 'request';
|
||||
import { sendTaskWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
groupChatReceivedWebhook,
|
||||
taskActivityWebhook,
|
||||
} from '../../../../../website/server/libs/webhook';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(request, 'post');
|
||||
|
||||
webhooks = [{
|
||||
id: 'taskActivity',
|
||||
url: 'http://task-scored.com',
|
||||
enabled: true,
|
||||
type: 'taskActivity',
|
||||
options: {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
},
|
||||
}, {
|
||||
id: 'groupChatReceived',
|
||||
url: 'http://group-chat-received.com',
|
||||
enabled: true,
|
||||
type: 'groupChatReceived',
|
||||
options: {
|
||||
groupId: 'group-id',
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('sendTaskWebhook', () => {
|
||||
let task = {
|
||||
details: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
direction: 'up',
|
||||
};
|
||||
describe('WebhookSender', () => {
|
||||
it('creates a new WebhookSender object', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let data = {
|
||||
task,
|
||||
user: { _id: 'user-id' },
|
||||
};
|
||||
expect(sendWebhook.type).to.equal('custom');
|
||||
expect(sendWebhook).to.respondTo('send');
|
||||
});
|
||||
|
||||
it('does not send if no webhook endpoints exist', () => {
|
||||
let webhooks = { };
|
||||
it('provides default function for data transformation', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body,
|
||||
});
|
||||
});
|
||||
|
||||
it('can pass in a data transformation function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
transformData (data) {
|
||||
let dataToSend = Object.assign({baz: 'biz'}, data);
|
||||
|
||||
return dataToSend;
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
foo: 'bar',
|
||||
baz: 'biz',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('provieds a default filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('can pass in a webhook filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook) {
|
||||
return hook.url !== 'http://custom-url.com';
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if no webhooks are enabled', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: false,
|
||||
url: 'http://example.org/endpoint',
|
||||
it('can pass in a webhook filter function that filters on data', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook, data) {
|
||||
return hook.options.foo === data.foo;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if webhook url is not valid', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://malformedurl/endpoint',
|
||||
},
|
||||
};
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('sends task direction, task, task delta, and abridged user data', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint',
|
||||
},
|
||||
};
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores disabled webhooks', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks with invalid urls', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks of other types', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a post request for each webhook endpoint', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint',
|
||||
},
|
||||
'second-webhook': {
|
||||
sort: 1,
|
||||
id: 'second-webhook',
|
||||
enabled: true,
|
||||
url: 'http://example.com/2/endpoint',
|
||||
},
|
||||
};
|
||||
it('sends multiple webhooks of the same type', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledTwice;
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.com/2/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://other-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskScoredWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toJSON () {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
addComputedStatsToJSONObj () {
|
||||
let mockStats = Object.assign({
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
toNextLevel: 40,
|
||||
}, this.stats);
|
||||
|
||||
delete mockStats.toJSON;
|
||||
|
||||
return mockStats;
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
};
|
||||
});
|
||||
|
||||
it('sends task and stats data', () => {
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
type: 'scored',
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toNextLevel: 40,
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send task scored data if scored option is not true', () => {
|
||||
webhooks[0].options.scored = false;
|
||||
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskActivityWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted'].forEach((type) => {
|
||||
it(`sends ${type} tasks`, () => {
|
||||
data.type = type;
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
type,
|
||||
task: data.task,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not send task ${type} data if ${type} option is not true`, () => {
|
||||
data.type = type;
|
||||
webhooks[0].options[type] = false;
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupChatReceivedWebhook', () => {
|
||||
it('sends chat data', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send chat data for group if not selected', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'not-group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
});
|
||||
expect(res.sendStatus).to.not.have.been.called;
|
||||
expect(next).to.have.been.called.once;
|
||||
@@ -32,7 +32,7 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
});
|
||||
expect(res.sendStatus).to.have.been.calledWith(200);
|
||||
expect(next).to.not.have.been.called;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { BadRequest } from '../../../../../website/server/libs/errors';
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import validator from 'validator';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
||||
@@ -433,6 +436,158 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateInvitations', () => {
|
||||
let res;
|
||||
|
||||
beforeEach(() => {
|
||||
res = {
|
||||
t: sandbox.spy(),
|
||||
};
|
||||
});
|
||||
|
||||
it('throws an error if no uuids or emails are passed in', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations({ uuid: 'user-id'}, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if only emails are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if total invites exceed max invite constant', (done) => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
|
||||
uuids.push(`user-id-${i}`);
|
||||
emails.push(`user-${i}@example.com`);
|
||||
}
|
||||
|
||||
uuids.push('one-more-uuid'); // to put it over the limit
|
||||
|
||||
try {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not throw error if number of invites matches max invite limit', () => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
|
||||
uuids.push(`user-id-${i}`);
|
||||
emails.push(`user-${i}@example.com`);
|
||||
}
|
||||
|
||||
expect(function () {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
}).to.not.throw();
|
||||
});
|
||||
|
||||
|
||||
it('does not throw an error if only user ids are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if only emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if both uuids and emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Instance Methods', () => {
|
||||
@@ -1064,5 +1219,163 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendGroupChatReceivedWebhooks', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(groupChatReceivedWebhook, 'send');
|
||||
});
|
||||
|
||||
it('looks for users in specified guild with webhooks', () => {
|
||||
sandbox.spy(User, 'find');
|
||||
|
||||
let guild = new Group({
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks({});
|
||||
|
||||
expect(User.find).to.be.calledWith({
|
||||
webhooks: {
|
||||
$elemMatch: {
|
||||
type: 'groupChatReceived',
|
||||
'options.groupId': guild._id,
|
||||
},
|
||||
},
|
||||
guilds: guild._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('looks for users in specified party with webhooks', () => {
|
||||
sandbox.spy(User, 'find');
|
||||
|
||||
party.sendGroupChatReceivedWebhooks({});
|
||||
|
||||
expect(User.find).to.be.calledWith({
|
||||
webhooks: {
|
||||
$elemMatch: {
|
||||
type: 'groupChatReceived',
|
||||
'options.groupId': party._id,
|
||||
},
|
||||
},
|
||||
'party._id': party._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends webhooks for users with webhooks', async () => {
|
||||
let guild = new Group({
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
let chat = {message: 'text'};
|
||||
let memberWithWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://someurl.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithoutWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
let nonMemberWithWebhooks = new User({
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://a-different-url.com',
|
||||
options: {
|
||||
groupId: generateUUID(),
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
memberWithWebhook.save(),
|
||||
memberWithoutWebhook.save(),
|
||||
nonMemberWithWebhooks.save(),
|
||||
]);
|
||||
|
||||
guild.leader = memberWithWebhook._id;
|
||||
|
||||
await guild.save();
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks(chat);
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
|
||||
|
||||
let args = groupChatReceivedWebhook.send.args[0];
|
||||
let webhooks = args[0];
|
||||
let options = args[1];
|
||||
|
||||
expect(webhooks).to.have.a.lengthOf(1);
|
||||
expect(webhooks[0].id).to.eql(memberWithWebhook.webhooks[0].id);
|
||||
expect(options.group).to.eql(guild);
|
||||
expect(options.chat).to.eql(chat);
|
||||
});
|
||||
|
||||
it('sends webhooks for each user with webhooks in group', async () => {
|
||||
let guild = new Group({
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
let chat = {message: 'text'};
|
||||
let memberWithWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://someurl.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithWebhook2 = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://another-member.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithWebhook3 = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://a-third-member.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
memberWithWebhook.save(),
|
||||
memberWithWebhook2.save(),
|
||||
memberWithWebhook3.save(),
|
||||
]);
|
||||
|
||||
guild.leader = memberWithWebhook._id;
|
||||
|
||||
await guild.save();
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks(chat);
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
|
||||
|
||||
let args = groupChatReceivedWebhook.send.args;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('User Model', () => {
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
user.addComputedStatsToJSONObj(userToJSON);
|
||||
user.addComputedStatsToJSONObj(userToJSON.stats);
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
|
||||
146
test/api/v3/unit/models/webhook.test.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { model as Webhook } from '../../../../../website/server/models/webhook';
|
||||
import { BadRequest } from '../../../../../website/server/libs/errors';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('Webhook Model', () => {
|
||||
context('Instance Methods', () => {
|
||||
describe('#formatOptions', () => {
|
||||
let res;
|
||||
|
||||
beforeEach(() => {
|
||||
res = {
|
||||
t: sandbox.spy(),
|
||||
};
|
||||
});
|
||||
context('type is taskActivity', () => {
|
||||
let config;
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
type: 'taskActivity',
|
||||
url: 'https//exmaple.com/endpoint',
|
||||
options: {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('it provides default values for options', () => {
|
||||
delete config.options;
|
||||
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
|
||||
expect(wh.options).to.eql({
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('provides missing task options', () => {
|
||||
delete config.options.created;
|
||||
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
|
||||
expect(wh.options).to.eql({
|
||||
created: false,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('discards additional options', () => {
|
||||
config.options.foo = 'another option';
|
||||
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
|
||||
expect(wh.options.foo).to.not.exist;
|
||||
expect(wh.options).to.eql({
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
});
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
|
||||
it(`validates that ${option} is a boolean`, (done) => {
|
||||
config.options[option] = 'not a boolean';
|
||||
|
||||
try {
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceOf(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('type is groupChatReceived', () => {
|
||||
let config;
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
type: 'groupChatReceived',
|
||||
url: 'https//exmaple.com/endpoint',
|
||||
options: {
|
||||
groupId: generateUUID(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('creates options', () => {
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
|
||||
expect(wh.options).to.eql(config.options);
|
||||
});
|
||||
|
||||
it('discards additional objects', () => {
|
||||
config.options.foo = 'another thing';
|
||||
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
|
||||
expect(wh.options.foo).to.not.exist;
|
||||
expect(wh.options).to.eql({
|
||||
groupId: config.options.groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('requires groupId option to be a uuid', (done) => {
|
||||
config.options.groupId = 'not a uuid';
|
||||
|
||||
try {
|
||||
let wh = new Webhook(config);
|
||||
|
||||
wh.formatOptions(res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceOf(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('groupIdRequired');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
describe('Auth Controller', function() {
|
||||
var scope, ctrl, user, $httpBackend, $window, $modal;
|
||||
var scope, ctrl, user, $httpBackend, $window, $modal, alert, Auth;
|
||||
|
||||
beforeEach(function(){
|
||||
module(function($provide) {
|
||||
Auth = {
|
||||
runAuth: sandbox.spy(),
|
||||
};
|
||||
$provide.value('Analytics', analyticsMock);
|
||||
$provide.value('Chat', { seenMessage: function() {} });
|
||||
$provide.value('Auth', Auth);
|
||||
});
|
||||
|
||||
inject(function(_$httpBackend_, $rootScope, $controller, _$modal_) {
|
||||
@@ -17,27 +21,27 @@ describe('Auth Controller', function() {
|
||||
$window = { location: { href: ""}, alert: sandbox.spy() };
|
||||
$modal = _$modal_;
|
||||
user = { user: {}, authenticate: sandbox.spy() };
|
||||
alert = { authErrorAlert: sandbox.spy() };
|
||||
|
||||
ctrl = $controller('AuthCtrl', {$scope: scope, $window: $window, User: user});
|
||||
ctrl = $controller('AuthCtrl', {$scope: scope, $window: $window, User: user, Alert: alert});
|
||||
})
|
||||
});
|
||||
|
||||
describe('logging in', function() {
|
||||
|
||||
it('should log in users with correct uname / pass', function() {
|
||||
$httpBackend.expectPOST('/api/v3/user/auth/local/login').respond({data: {id: 'abc', apiToken: 'abc'}});
|
||||
scope.auth();
|
||||
$httpBackend.flush();
|
||||
expect(user.authenticate).to.be.calledOnce;
|
||||
expect($window.alert).to.not.be.called;
|
||||
expect(Auth.runAuth).to.be.calledOnce;
|
||||
expect(alert.authErrorAlert).to.not.be.called;
|
||||
});
|
||||
|
||||
it('should not log in users with incorrect uname / pass', function() {
|
||||
$httpBackend.expectPOST('/api/v3/user/auth/local/login').respond(404, '');
|
||||
scope.auth();
|
||||
$httpBackend.flush();
|
||||
expect(user.authenticate).to.not.be.called;
|
||||
expect($window.alert).to.be.calledOnce;
|
||||
expect(Auth.runAuth).to.not.be.called;
|
||||
expect(alert.authErrorAlert).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Footer Controller', function() {
|
||||
user: user
|
||||
};
|
||||
scope = $rootScope.$new();
|
||||
$controller('FooterCtrl', {$scope: scope, User: User});
|
||||
$controller('FooterCtrl', {$scope: scope, User: User, Social: {}});
|
||||
}));
|
||||
|
||||
context('Debug mode', function() {
|
||||
|
||||
@@ -193,6 +193,7 @@ describe('Analytics Service', function () {
|
||||
todos: 1,
|
||||
rewards: 1
|
||||
};
|
||||
expectedProperties.balance = 12;
|
||||
|
||||
beforeEach(function() {
|
||||
user._id = 'unique-user-id';
|
||||
@@ -207,6 +208,7 @@ describe('Analytics Service', function () {
|
||||
user.dailys = [{_id: 'daily'}];
|
||||
user.todos = [{_id: 'todo'}];
|
||||
user.rewards = [{_id: 'reward'}];
|
||||
user.balance = 12;
|
||||
|
||||
analytics.updateUser(properties);
|
||||
clock.tick();
|
||||
@@ -240,7 +242,8 @@ describe('Analytics Service', function () {
|
||||
dailys: 1,
|
||||
habits: 1,
|
||||
rewards: 1
|
||||
}
|
||||
},
|
||||
balance: 12
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -258,6 +261,7 @@ describe('Analytics Service', function () {
|
||||
user.dailys = [{_id: 'daily'}];
|
||||
user.todos = [{_id: 'todo'}];
|
||||
user.rewards = [{_id: 'reward'}];
|
||||
user.balance = 12;
|
||||
|
||||
analytics.updateUser();
|
||||
clock.tick();
|
||||
|
||||
5
test/client/.babelrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"comments": false
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
// import Hello from 'src/components/Hello';
|
||||
|
||||
describe('Hello.vue', () => {
|
||||
xit('should render correct contents', () => {
|
||||
const vm = new Vue({
|
||||
el: document.createElement('div'),
|
||||
render: (h) => h(Hello),
|
||||
});
|
||||
expect(vm.$el.querySelector('.hello h1').textContent).to.equal('Hello Vue!');
|
||||
});
|
||||
|
||||
it('should make assertions', () => {
|
||||
expect(true).to.equal(true);
|
||||
});
|
||||
});
|
||||
120
test/client/unit/specs/store.spec.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import Vue from 'vue';
|
||||
import storeInjector from 'inject?-vue!client/store';
|
||||
import { mapState, mapGetters, mapActions } from 'client/store';
|
||||
|
||||
describe('Store', () => {
|
||||
let injectedStore;
|
||||
|
||||
beforeEach(() => {
|
||||
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
|
||||
'./state': {
|
||||
name: 'test',
|
||||
},
|
||||
'./getters': {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
},
|
||||
'./actions': {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
}).default;
|
||||
});
|
||||
|
||||
it('injects itself in all component', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
created () {
|
||||
expect(this.$store).to.equal(injectedStore);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can watch a function on the state', (done) => {
|
||||
injectedStore.watch(state => state.name, (newName) => {
|
||||
expect(newName).to.equal('test updated');
|
||||
done();
|
||||
});
|
||||
|
||||
injectedStore.state.name = 'test updated';
|
||||
});
|
||||
|
||||
it('supports getters', () => {
|
||||
expect(injectedStore.getters.computedName).to.equal('test computed!');
|
||||
injectedStore.state.name = 'test updated';
|
||||
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('can be dispatched', () => {
|
||||
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('throws an error is the action doesn\'t exists', () => {
|
||||
expect(() => injectedStore.dispatched('wrong')).to.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe('helpers', () => {
|
||||
it('mapState', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
data: {
|
||||
title: 'internal',
|
||||
},
|
||||
computed: {
|
||||
...mapState(['name']),
|
||||
...mapState({
|
||||
nameComputed (state, getters) {
|
||||
return `${this.title} ${getters.computedName} ${state.name}`;
|
||||
},
|
||||
}),
|
||||
},
|
||||
created () {
|
||||
expect(this.name).to.equal('test');
|
||||
expect(this.nameComputed).to.equal('internal test computed! test');
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('mapGetters', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
data: {
|
||||
title: 'internal',
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['computedName']),
|
||||
...mapGetters({
|
||||
nameComputedTwice: 'computedName',
|
||||
}),
|
||||
},
|
||||
created () {
|
||||
expect(this.computedName).to.equal('test computed!');
|
||||
expect(this.nameComputedTwice).to.equal('test computed!');
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('mapActions', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
data: {
|
||||
title: 'internal',
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['getName']),
|
||||
...mapActions({
|
||||
getNameRenamed: 'getName',
|
||||
}),
|
||||
},
|
||||
created () {
|
||||
expect(this.getName('123')).to.deep.equal(['test', '123']);
|
||||
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import randomVal from '../../../website/common/script/libs/randomVal';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('randomVal', () => {
|
||||
let obj;
|
||||
|
||||
beforeEach(() => {
|
||||
obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('returns a random value from an object', () => {
|
||||
let result = randomVal(obj);
|
||||
expect(result).to.be.oneOf([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('uses Math.random to determine the property', () => {
|
||||
sandbox.spy(Math, 'random');
|
||||
|
||||
randomVal(obj);
|
||||
|
||||
expect(Math.random).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('can pass in a custom random function that takes in the user and a seed argument', () => {
|
||||
let user = generateUser();
|
||||
let randomSpy = sandbox.stub().returns(0.3);
|
||||
sandbox.spy(Math, 'random');
|
||||
|
||||
let result = randomVal(obj, {
|
||||
user,
|
||||
seed: 100,
|
||||
predictableRandom: randomSpy,
|
||||
});
|
||||
|
||||
expect(Math.random).to.not.be.called;
|
||||
expect(randomSpy).to.be.calledOnce;
|
||||
expect(result).to.equal(2);
|
||||
});
|
||||
|
||||
it('returns a random key when the key option is passed in', () => {
|
||||
let result = randomVal(obj, { key: true });
|
||||
expect(result).to.be.oneOf(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,8 @@ describe('shared.fns.ultimateGear', () => {
|
||||
expect(user.addNotification).to.be.calledWith('ULTIMATE_GEAR_ACHIEVEMENT');
|
||||
});
|
||||
|
||||
it('does not set armoirEnabled when gear is not owned', () => {
|
||||
it('does not set armoireEnabled when gear is not owned', () => {
|
||||
user.flags.armoireEnabled = false;
|
||||
let items = {
|
||||
gear: {
|
||||
owned: {
|
||||
|
||||
37
test/common/libs/randomVal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import randomVal from '../../../website/common/script/libs/randomVal';
|
||||
import {times} from 'lodash';
|
||||
|
||||
describe('randomVal', () => {
|
||||
let obj;
|
||||
|
||||
beforeEach(() => {
|
||||
obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('returns a random value from an object', () => {
|
||||
let result = randomVal(obj);
|
||||
expect(result).to.be.oneOf([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('can pass in a predictable random value', () => {
|
||||
times(30, () => {
|
||||
expect(randomVal(obj, {
|
||||
predictableRandom: 0.3,
|
||||
})).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a random key when the key option is passed in', () => {
|
||||
let result = randomVal(obj, { key: true });
|
||||
expect(result).to.be.oneOf(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,9 @@ describe('shops', () => {
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
_.each(category.items, (item) => {
|
||||
expect(item).to.have.all.keys(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class']);
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,7 +48,9 @@ describe('shops', () => {
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
_.each(category.items, (item) => {
|
||||
expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl');
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -70,7 +74,9 @@ describe('shops', () => {
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
_.each(category.items, (item) => {
|
||||
expect(item).to.have.all.keys('key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class');
|
||||
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -94,7 +100,9 @@ describe('shops', () => {
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
_.each(category.items, (item) => {
|
||||
expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'specialClass', 'type');
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import addWebhook from '../../../website/common/script/ops/addWebhook';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.addWebhook', () => {
|
||||
let user;
|
||||
let req;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
req = { body: {
|
||||
enabled: true,
|
||||
url: 'http://some-url.com',
|
||||
} };
|
||||
});
|
||||
|
||||
context('adds webhook', () => {
|
||||
it('validates req.body.url', (done) => {
|
||||
delete req.body.url;
|
||||
try {
|
||||
addWebhook(user, req);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUrl'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('validates req.body.enabled', (done) => {
|
||||
delete req.body.enabled;
|
||||
try {
|
||||
addWebhook(user, req);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidEnabled'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('calls marksModified()', () => {
|
||||
user.markModified = sinon.spy();
|
||||
addWebhook(user, req);
|
||||
expect(user.markModified.called).to.eql(true);
|
||||
});
|
||||
|
||||
it('succeeds', () => {
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
addWebhook(user, req);
|
||||
expect(user.preferences.webhooks).to.not.eql({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '../../helpers/common.helper';
|
||||
import count from '../../../website/common/script/count';
|
||||
import buyArmoire from '../../../website/common/script/ops/buyArmoire';
|
||||
import randomVal from '../../../website/common/script/libs/randomVal';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
import {
|
||||
NotAuthorized,
|
||||
@@ -43,11 +44,11 @@ describe('shared.ops.buyArmoire', () => {
|
||||
user.stats.exp = 0;
|
||||
user.items.food = {};
|
||||
|
||||
sandbox.stub(Math, 'random');
|
||||
sandbox.stub(randomVal, 'trueRandom');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Math.random.restore();
|
||||
randomVal.trueRandom.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -89,7 +90,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
context('non-gear awards', () => {
|
||||
it('gives Experience', () => {
|
||||
let previousExp = user.stats.exp;
|
||||
Math.random.returns(YIELD_EXP);
|
||||
randomVal.trueRandom.returns(YIELD_EXP);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
@@ -102,7 +103,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
it('gives food', () => {
|
||||
let previousExp = user.stats.exp;
|
||||
|
||||
Math.random.returns(YIELD_FOOD);
|
||||
randomVal.trueRandom.returns(YIELD_FOOD);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
@@ -113,7 +114,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
});
|
||||
|
||||
it('does not give equipment if all equipment has been found', () => {
|
||||
Math.random.returns(YIELD_EQUIPMENT);
|
||||
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
|
||||
user.items.gear.owned = getFullArmoire();
|
||||
user.stats.gp = 150;
|
||||
|
||||
@@ -131,7 +132,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
context('gear awards', () => {
|
||||
it('always drops equipment the first time', () => {
|
||||
delete user.flags.armoireOpened;
|
||||
Math.random.returns(YIELD_EXP);
|
||||
randomVal.trueRandom.returns(YIELD_EXP);
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(1);
|
||||
|
||||
@@ -148,7 +149,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
});
|
||||
|
||||
it('gives more equipment', () => {
|
||||
Math.random.returns(YIELD_EQUIPMENT);
|
||||
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
|
||||
user.items.gear.owned = {
|
||||
weapon_warrior_0: true,
|
||||
head_armoire_hornedIronHelm: true,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import deleteWebhook from '../../../website/common/script/ops/deleteWebhook';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.deleteWebhook', () => {
|
||||
let user;
|
||||
let req;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
req = { params: { id: 'some-id' } };
|
||||
});
|
||||
|
||||
it('succeeds', () => {
|
||||
user.preferences.webhooks = { 'some-id': {}, 'another-id': {} };
|
||||
let [data] = deleteWebhook(user, req);
|
||||
expect(user.preferences.webhooks).to.eql({'another-id': {}});
|
||||
expect(data).to.equal(user.preferences.webhooks);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import releaseBoth from '../../../website/common/script/ops/releaseBoth';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
@@ -65,19 +66,41 @@ describe('shared.ops.releaseBoth', () => {
|
||||
expect(user.items.mounts[animal]).to.equal(null);
|
||||
});
|
||||
|
||||
it('removes currentPet', () => {
|
||||
it('removes drop currentPet', () => {
|
||||
let petInfo = content.petInfo[user.items.currentPet];
|
||||
expect(petInfo.type).to.equal('drop');
|
||||
releaseBoth(user);
|
||||
|
||||
expect(user.items.currentMount).to.be.empty;
|
||||
expect(user.items.currentPet).to.be.empty;
|
||||
});
|
||||
|
||||
it('removes currentMount', () => {
|
||||
it('removes drop currentMount', () => {
|
||||
let mountInfo = content.mountInfo[user.items.currentMount];
|
||||
expect(mountInfo.type).to.equal('drop');
|
||||
releaseBoth(user);
|
||||
|
||||
expect(user.items.currentMount).to.be.empty;
|
||||
});
|
||||
|
||||
it('leaves non-drop pets and mounts equipped', () => {
|
||||
let questAnimal = 'Gryphon-Base';
|
||||
user.items.currentMount = questAnimal;
|
||||
user.items.currentPet = questAnimal;
|
||||
user.items.pets[questAnimal] = 5;
|
||||
user.items.mounts[questAnimal] = true;
|
||||
|
||||
let petInfo = content.petInfo[user.items.currentPet];
|
||||
expect(petInfo.type).to.not.equal('drop');
|
||||
let mountInfo = content.mountInfo[user.items.currentMount];
|
||||
expect(mountInfo.type).to.not.equal('drop');
|
||||
|
||||
releaseBoth(user);
|
||||
|
||||
expect(user.items.currentMount).to.equal(questAnimal);
|
||||
expect(user.items.currentPet).to.equal(questAnimal);
|
||||
});
|
||||
|
||||
it('decreases user\'s balance', () => {
|
||||
releaseBoth(user);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import releaseMounts from '../../../website/common/script/ops/releaseMounts';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
@@ -37,12 +38,26 @@ describe('shared.ops.releaseMounts', () => {
|
||||
expect(user.items.mounts[animal]).to.equal(null);
|
||||
});
|
||||
|
||||
it('removes currentMount', () => {
|
||||
it('removes drop currentMount', () => {
|
||||
let mountInfo = content.mountInfo[user.items.currentMount];
|
||||
expect(mountInfo.type).to.equal('drop');
|
||||
releaseMounts(user);
|
||||
|
||||
expect(user.items.currentMount).to.be.empty;
|
||||
});
|
||||
|
||||
it('leaves non-drop mount equipped', () => {
|
||||
let questAnimal = 'Gryphon-Base';
|
||||
user.items.currentMount = questAnimal;
|
||||
user.items.mounts[questAnimal] = true;
|
||||
|
||||
let mountInfo = content.mountInfo[user.items.currentMount];
|
||||
expect(mountInfo.type).to.not.equal('drop');
|
||||
releaseMounts(user);
|
||||
|
||||
expect(user.items.currentMount).to.equal(questAnimal);
|
||||
});
|
||||
|
||||
it('increases mountMasterCount achievement', () => {
|
||||
releaseMounts(user);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import releasePets from '../../../website/common/script/ops/releasePets';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
@@ -37,12 +38,26 @@ describe('shared.ops.releasePets', () => {
|
||||
expect(user.items.pets[animal]).to.equal(0);
|
||||
});
|
||||
|
||||
it('removes currentPet', () => {
|
||||
it('removes drop currentPet', () => {
|
||||
let petInfo = content.petInfo[user.items.currentPet];
|
||||
expect(petInfo.type).to.equal('drop');
|
||||
releasePets(user);
|
||||
|
||||
expect(user.items.currentPet).to.be.empty;
|
||||
});
|
||||
|
||||
it('leaves non-drop pets equipped', () => {
|
||||
let questAnimal = 'Gryphon-Base';
|
||||
user.items.currentPet = questAnimal;
|
||||
user.items.pets[questAnimal] = 5;
|
||||
|
||||
let petInfo = content.petInfo[user.items.currentPet];
|
||||
expect(petInfo.type).to.not.equal('drop');
|
||||
releasePets(user);
|
||||
|
||||
expect(user.items.currentPet).to.equal(questAnimal);
|
||||
});
|
||||
|
||||
it('decreases user\'s balance', () => {
|
||||
releasePets(user);
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ describe('shared.ops.revive', () => {
|
||||
expect(user.stats.str).to.equal(1);
|
||||
});
|
||||
|
||||
it('TODO: test actual ways stats are affected');
|
||||
|
||||
it('removes a random item from user gear owned', () => {
|
||||
let weaponKey = 'weapon_warrior_0';
|
||||
user.items.gear.owned[weaponKey] = true;
|
||||
@@ -63,7 +65,17 @@ describe('shared.ops.revive', () => {
|
||||
expect(user.items.gear.owned[weaponKey]).to.be.false;
|
||||
});
|
||||
|
||||
it('removes a random item from user gear equipped', () => {
|
||||
it('does not remove 0 value items');
|
||||
|
||||
it('allows removing warrior sword (0 value item)');
|
||||
|
||||
it('does not remove items of a different class');
|
||||
|
||||
it('removes "special" items');
|
||||
|
||||
it('removes "armoire" items');
|
||||
|
||||
it('dequips lost item from user if user had it equipped', () => {
|
||||
let weaponKey = 'weapon_warrior_0';
|
||||
let itemToLose = content.gear.flat[weaponKey];
|
||||
|
||||
@@ -76,7 +88,7 @@ describe('shared.ops.revive', () => {
|
||||
expect(user.items.gear.equipped[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`);
|
||||
});
|
||||
|
||||
it('removes a random item from user gear costume', () => {
|
||||
it('dequips lost item from user costume if user was using it in costume', () => {
|
||||
let weaponKey = 'weapon_warrior_0';
|
||||
let itemToLose = content.gear.flat[weaponKey];
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import updateWebhook from '../../../website/common/script/ops/updateWebhook';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.updateWebhook', () => {
|
||||
let user;
|
||||
let req;
|
||||
let newUrl = 'http://new-url.com';
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
req = { params: {
|
||||
id: 'this-id',
|
||||
}, body: {
|
||||
url: newUrl,
|
||||
enabled: true,
|
||||
} };
|
||||
});
|
||||
|
||||
it('validates body', (done) => {
|
||||
delete req.body.url;
|
||||
try {
|
||||
updateWebhook(user, req);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUrl'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('succeeds', () => {
|
||||
let url = 'http://existing-url.com';
|
||||
user.preferences.webhooks = { 'this-id': { url } };
|
||||
updateWebhook(user, req);
|
||||
expect(user.preferences.webhooks['this-id'].url).to.eql(newUrl);
|
||||
});
|
||||
});
|
||||
70
test/helpers/api-integration/v3/external-server.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let uuid = require('uuid');
|
||||
let bodyParser = require('body-parser');
|
||||
let app = express();
|
||||
let server = require('http').createServer(app);
|
||||
|
||||
const PORT = process.env.TEST_WEBHOOK_APP_PORT || 3099; // eslint-disable-line no-process-env
|
||||
|
||||
let webhookData = {};
|
||||
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true,
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.post('/webhooks/:id', function (req, res) {
|
||||
let id = req.params.id;
|
||||
|
||||
if (!webhookData[id]) {
|
||||
webhookData[id] = [];
|
||||
}
|
||||
|
||||
webhookData[id].push(req.body);
|
||||
|
||||
res.status(200);
|
||||
});
|
||||
|
||||
// Helps close down server from within mocha test
|
||||
// See http://stackoverflow.com/a/37054753/2601552
|
||||
let sockets = {};
|
||||
server.on('connection', (socket) => {
|
||||
let id = uuid.v4();
|
||||
sockets[id] = socket;
|
||||
|
||||
socket.once('close', () => {
|
||||
delete sockets[id];
|
||||
});
|
||||
});
|
||||
|
||||
function start () {
|
||||
return new Promise((resolve) => {
|
||||
server.listen(PORT, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function close () {
|
||||
return new Promise((resolve) => {
|
||||
server.close(resolve);
|
||||
|
||||
Object.keys(sockets).forEach((socket) => {
|
||||
sockets[socket].end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWebhookData (id) {
|
||||
if (!webhookData[id]) {
|
||||
return null;
|
||||
}
|
||||
return webhookData[id].pop();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
close,
|
||||
getWebhookData,
|
||||
port: PORT,
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
// Import requester function, set it up for v2, export it
|
||||
// Import requester function, set it up for v3, export it
|
||||
import { requester } from '../requester';
|
||||
requester.setApiVersion('v3');
|
||||
export { requester };
|
||||
|
||||
export { translate } from '../translate';
|
||||
import server from './external-server';
|
||||
export { server };
|
||||
|
||||
export { translate } from '../../translate';
|
||||
export { checkExistence, getProperty, resetHabiticaDB } from '../../mongo';
|
||||
export * from './object-generators';
|
||||
export { sleep } from '../../sleep';
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RewardSchema,
|
||||
TodoSchema,
|
||||
} from '../../website/server/models/task';
|
||||
export {translate} from './translate';
|
||||
|
||||
export function generateUser (options = {}) {
|
||||
let user = new User(options).toObject();
|
||||
|
||||
@@ -15,25 +15,3 @@ global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
global.sandbox = sinon.sandbox.create();
|
||||
global.Promise = Bluebird;
|
||||
|
||||
import nconf from 'nconf';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
//------------------------------
|
||||
// Load nconf for unit tests
|
||||
//------------------------------
|
||||
if (process.env.LOAD_SERVER === '0') { // when the server is in a different process we simply connect to mongoose
|
||||
require('../../website/server/libs/setupNconf')('./config.json');
|
||||
// Use Q promises instead of mpromise in mongoose
|
||||
mongoose.Promise = Bluebird;
|
||||
mongoose.connect(nconf.get('TEST_DB_URI'));
|
||||
} else { // When running tests and the server in the same process
|
||||
require('../../website/server/libs/setupNconf')('./config.json.example');
|
||||
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
|
||||
nconf.set('NODE_ENV', 'test');
|
||||
nconf.set('IS_TEST', true);
|
||||
// We require src/server and npt src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
require('../../website/server/server');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export async function sleep (seconds) {
|
||||
export async function sleep (seconds = 1) {
|
||||
let milliseconds = seconds * 1000;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
21
test/helpers/start-server.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable no-process-env */
|
||||
import nconf from 'nconf';
|
||||
import mongoose from 'mongoose';
|
||||
import Bluebird from 'bluebird';
|
||||
import setupNconf from '../../website/server/libs/setupNconf';
|
||||
|
||||
if (process.env.LOAD_SERVER === '0') { // when the server is in a different process we simply connect to mongoose
|
||||
setupNconf('./config.json');
|
||||
// Use Q promises instead of mpromise in mongoose
|
||||
mongoose.Promise = Bluebird;
|
||||
mongoose.connect(nconf.get('TEST_DB_URI'));
|
||||
} else { // When running tests and the server in the same process
|
||||
setupNconf('./config.json.example');
|
||||
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
|
||||
nconf.set('NODE_ENV', 'test');
|
||||
nconf.set('IS_TEST', true);
|
||||
// We require src/server and npt src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
require('../../website/server/server'); // eslint-disable-line global-require
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
i18n.translations = require('../../../website/server/libs/i18n').translations;
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
i18n.translations = require('../../website/server/libs/i18n').translations;
|
||||
|
||||
const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
|
||||
const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
// Use this to verify error messages returned by the server
|
||||
// That way, if the translated string changes, the test
|
||||
// will not break. NOTE: it checks against errors with string as well.
|
||||
export function translate (key, variables) {
|
||||
const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
|
||||
const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
let translatedString = i18n.t(key, variables);
|
||||
|
||||
expect(translatedString).to.not.be.empty;
|
||||
@@ -1,467 +0,0 @@
|
||||
var sinon = require('sinon');
|
||||
var chai = require("chai")
|
||||
chai.use(require("sinon-chai"))
|
||||
var expect = chai.expect
|
||||
var rewire = require('rewire');
|
||||
|
||||
describe('analytics', function() {
|
||||
// Mocks
|
||||
var amplitudeMock = sinon.stub();
|
||||
var googleAnalyticsMock = sinon.stub();
|
||||
var amplitudeTrack = sinon.stub().returns({
|
||||
catch: function () { return true; }
|
||||
});
|
||||
var googleEvent = sinon.stub().returns({
|
||||
send: function() { }
|
||||
});
|
||||
var googleItem = sinon.stub().returns({
|
||||
send: function() { }
|
||||
});
|
||||
var googleTransaction = sinon.stub().returns({
|
||||
item: googleItem
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
amplitudeMock.reset();
|
||||
amplitudeTrack.reset();
|
||||
googleEvent.reset();
|
||||
googleTransaction.reset();
|
||||
googleItem.reset();
|
||||
});
|
||||
|
||||
describe('init', function() {
|
||||
var analytics = rewire('../../website/server/libs/api-v2/analytics');
|
||||
|
||||
it('throws an error if no options are passed in', function() {
|
||||
expect(analytics).to.throw('No options provided');
|
||||
});
|
||||
|
||||
it('registers amplitude with token', function() {
|
||||
analytics.__set__('Amplitude', amplitudeMock);
|
||||
var options = {
|
||||
amplitudeToken: 'token'
|
||||
};
|
||||
analytics(options);
|
||||
|
||||
expect(amplitudeMock).to.be.calledOnce;
|
||||
expect(amplitudeMock).to.be.calledWith('token');
|
||||
});
|
||||
|
||||
it('registers google analytics with token', function() {
|
||||
analytics.__set__('googleAnalytics', googleAnalyticsMock);
|
||||
var options = {
|
||||
googleAnalytics: 'token'
|
||||
};
|
||||
analytics(options);
|
||||
|
||||
expect(googleAnalyticsMock).to.be.calledOnce;
|
||||
expect(googleAnalyticsMock).to.be.calledWith('token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('track', function() {
|
||||
|
||||
var analyticsData, event_type;
|
||||
var analytics = rewire('../../website/server/libs/api-v2/analytics');
|
||||
var initializedAnalytics;
|
||||
|
||||
beforeEach(function() {
|
||||
analytics.__set__('Amplitude', amplitudeMock);
|
||||
initializedAnalytics = analytics({amplitudeToken: 'token'});
|
||||
analytics.__set__('amplitude.track', amplitudeTrack);
|
||||
analytics.__set__('ga.event', googleEvent);
|
||||
|
||||
event_type = 'Cron';
|
||||
analyticsData = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
|
||||
context('Amplitude', function() {
|
||||
it('tracks event in amplitude', function() {
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('uses a dummy user id if none is provided', function() {
|
||||
delete analyticsData.uuid;
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'no-user-id-was-provided',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'headAccessory_special_foxEars'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'headAccessory_special_foxEars',
|
||||
itemName: 'Fox Ears',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'Wolf'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'Wolf',
|
||||
itemName: 'Wolf Egg',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'Cake_Skeleton'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'Cake_Skeleton',
|
||||
itemName: 'Bare Bones Cake',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'Golden'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'Golden',
|
||||
itemName: 'Golden Hatching Potion',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'atom1'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'atom1',
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', function() {
|
||||
analyticsData.itemKey = 'seafoam'
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
itemKey: 'seafoam',
|
||||
itemName: 'Seafoam',
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', function() {
|
||||
var stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
|
||||
var user = {
|
||||
stats: stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: {tour: {intro: -2}},
|
||||
habits: [{_id: 'habit'}],
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}]
|
||||
};
|
||||
|
||||
analyticsData.user = user;
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'Cron',
|
||||
user_id: 'unique-user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5
|
||||
},
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
tutorialComplete: true,
|
||||
"Number Of Tasks": {
|
||||
todos: 1,
|
||||
dailys: 1,
|
||||
habits: 1,
|
||||
rewards: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Google Analytics', function() {
|
||||
it('tracks event in google analytics', function() {
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron'
|
||||
});
|
||||
});
|
||||
|
||||
it('if itemKey property is provided, use as label', function() {
|
||||
analyticsData.itemKey = 'some item';
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron',
|
||||
el: 'some item'
|
||||
});
|
||||
});
|
||||
|
||||
it('if gaLabel property is provided, use as label (overrides itemKey)', function() {
|
||||
analyticsData.value = 'some value';
|
||||
analyticsData.itemKey = 'some item';
|
||||
analyticsData.gaLabel = 'some label';
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron',
|
||||
el: 'some label'
|
||||
});
|
||||
});
|
||||
|
||||
it('if goldCost property is provided, use as value', function() {
|
||||
analyticsData.goldCost = 5;
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron',
|
||||
ev: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('if gemCost property is provided, use as value (overrides goldCost)', function() {
|
||||
analyticsData.gemCost = 7;
|
||||
analyticsData.goldCost = 5;
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron',
|
||||
ev: 7
|
||||
});
|
||||
});
|
||||
|
||||
it('if gaValue property is provided, use as value (overrides gemCost)', function() {
|
||||
analyticsData.gemCost = 7;
|
||||
analyticsData.gaValue = 5;
|
||||
|
||||
initializedAnalytics.track(event_type, analyticsData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'behavior',
|
||||
ea: 'Cron',
|
||||
ev: 5
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackPurchase', function() {
|
||||
|
||||
var purchaseData;
|
||||
|
||||
var analytics = rewire('../../website/server/libs/api-v2/analytics');
|
||||
var initializedAnalytics;
|
||||
|
||||
beforeEach(function() {
|
||||
analytics.__set__('Amplitude', amplitudeMock);
|
||||
initializedAnalytics = analytics({amplitudeToken: 'token', googleAnalytics: 'token'});
|
||||
analytics.__set__('amplitude.track', amplitudeTrack);
|
||||
analytics.__set__('ga.event', googleEvent);
|
||||
analytics.__set__('ga.transaction', googleTransaction);
|
||||
|
||||
purchaseData = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
context('Amplitude', function() {
|
||||
|
||||
it('calls amplitude.track', function() {
|
||||
initializedAnalytics.trackPurchase(purchaseData);
|
||||
|
||||
expect(amplitudeTrack).to.be.calledOnce;
|
||||
expect(amplitudeTrack).to.be.calledWith({
|
||||
event_type: 'purchase',
|
||||
user_id: 'user-id',
|
||||
platform: 'server',
|
||||
event_properties: {
|
||||
paymentMethod: 'PayPal',
|
||||
sku: 'paypal-checkout',
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1
|
||||
},
|
||||
revenue: 8
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Google Analytics', function() {
|
||||
|
||||
it('calls ga.event', function() {
|
||||
initializedAnalytics.trackPurchase(purchaseData);
|
||||
|
||||
expect(googleEvent).to.be.calledOnce;
|
||||
expect(googleEvent).to.be.calledWith({
|
||||
ec: 'commerce',
|
||||
ea: 'checkout',
|
||||
el: 'PayPal',
|
||||
ev: 8
|
||||
});
|
||||
});
|
||||
|
||||
it('calls ga.transaction', function() {
|
||||
initializedAnalytics.trackPurchase(purchaseData);
|
||||
|
||||
expect(googleTransaction).to.be.calledOnce;
|
||||
expect(googleTransaction).to.be.calledWith(
|
||||
'user-id',
|
||||
8
|
||||
);
|
||||
expect(googleItem).to.be.calledOnce;
|
||||
expect(googleItem).to.be.calledWith(
|
||||
8,
|
||||
1,
|
||||
'paypal-checkout',
|
||||
'Gems',
|
||||
'checkout'
|
||||
);
|
||||
});
|
||||
|
||||
it('appends gift to variation of ga.transaction.item if gift is true', function() {
|
||||
|
||||
purchaseData.gift = true;
|
||||
initializedAnalytics.trackPurchase(purchaseData);
|
||||
|
||||
expect(googleItem).to.be.calledOnce;
|
||||
expect(googleItem).to.be.calledWith(
|
||||
8,
|
||||
1,
|
||||
'paypal-checkout',
|
||||
'Gems',
|
||||
'checkout - Gift'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,497 +0,0 @@
|
||||
var sinon = require('sinon');
|
||||
var chai = require("chai");
|
||||
chai.use(require("sinon-chai"));
|
||||
var expect = chai.expect;
|
||||
|
||||
var Bluebird = require('bluebird');
|
||||
var Group = require('../../../website/server/models/group').model;
|
||||
var groupsController = require('../../../website/server/controllers/api-v2/groups');
|
||||
|
||||
describe('Groups Controller', function() {
|
||||
var utils = require('../../../website/server/libs/api-v2/utils');
|
||||
|
||||
describe('#invite', function() {
|
||||
var res, req, user, group;
|
||||
|
||||
beforeEach(function() {
|
||||
group = {
|
||||
_id: 'group-id',
|
||||
name: 'group-name',
|
||||
type: 'party',
|
||||
members: [
|
||||
'user-id',
|
||||
'another-user'
|
||||
],
|
||||
save: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
name: 'inviter',
|
||||
email: 'inviter@example.com',
|
||||
save: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
res = {
|
||||
locals: {
|
||||
group: group,
|
||||
user: user
|
||||
},
|
||||
json: sinon.stub(),
|
||||
sendStatus: sinon.stub()
|
||||
};
|
||||
|
||||
req = {
|
||||
body: {}
|
||||
};
|
||||
});
|
||||
|
||||
context('uuids', function() {
|
||||
beforeEach(function() {
|
||||
req.body.uuids = ['invited-user'];
|
||||
});
|
||||
|
||||
it('returns 400 if user not found');
|
||||
|
||||
it('returns a 400 if user is already in the group');
|
||||
|
||||
it('retuns 400 if user was already invited to that group');
|
||||
|
||||
it('returns 400 if user is already pending an invitation');
|
||||
|
||||
it('returns 400 is user is already in another party');
|
||||
|
||||
it('emails invited user');
|
||||
|
||||
it('does not email invited user if email preference is set to false');
|
||||
});
|
||||
|
||||
context('emails', function() {
|
||||
var EmailUnsubscription = require('../../../website/server/models/emailUnsubscription').model;
|
||||
var execStub, selectStub;
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(utils, 'encrypt').returns('http://link.com');
|
||||
sinon.stub(utils, 'getUserInfo').returns({
|
||||
name: user.name,
|
||||
email: user.email
|
||||
});
|
||||
execStub = sinon.stub();
|
||||
selectStub = sinon.stub().returns({
|
||||
exec: execStub
|
||||
});
|
||||
sinon.stub(User, 'findOne').returns({
|
||||
select: selectStub
|
||||
});
|
||||
sinon.stub(EmailUnsubscription, 'findOne');
|
||||
sinon.stub(utils, 'txnEmail');
|
||||
|
||||
req.body.emails = [{email: 'user@example.com', name: 'user'}];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
User.findOne.restore();
|
||||
EmailUnsubscription.findOne.restore();
|
||||
utils.encrypt.restore();
|
||||
utils.getUserInfo.restore();
|
||||
utils.txnEmail.restore();
|
||||
});
|
||||
|
||||
it('emails user with invite', function() {
|
||||
execStub.yields(null, null);
|
||||
EmailUnsubscription.findOne.yields(null, null);
|
||||
|
||||
groupsController.invite(req, res);
|
||||
|
||||
expect(utils.txnEmail).to.be.calledOnce;
|
||||
expect(utils.txnEmail).to.be.calledWith(
|
||||
{ email: 'user@example.com', name: 'user' },
|
||||
'invite-friend',
|
||||
[
|
||||
{ name: 'LINK', content: '?partyInvite=http://link.com' },
|
||||
{ name: 'INVITER', content: 'inviter' }
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('does not email user if user is on unsubscribe list', function() {
|
||||
EmailUnsubscription.findOne.yields(null, {_id: 'on-list'});
|
||||
|
||||
expect(utils.txnEmail).to.not.be.called;
|
||||
});
|
||||
|
||||
it('checks if a user with provided email already exists');
|
||||
});
|
||||
|
||||
context('others', function() {
|
||||
it ('returns a 400 error', function() {
|
||||
groupsController.invite(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(
|
||||
400,
|
||||
{ err: 'Can invite only by email or uuid' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#leave', function() {
|
||||
var res, req, user, group;
|
||||
|
||||
beforeEach(function() {
|
||||
group = {
|
||||
_id: 'group-id',
|
||||
type: 'party',
|
||||
members: [
|
||||
'user-id',
|
||||
'another-user'
|
||||
],
|
||||
save: sinon.stub().yields(),
|
||||
leave: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
save: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
res = {
|
||||
locals: {
|
||||
group: group,
|
||||
user: user
|
||||
},
|
||||
json: sinon.stub(),
|
||||
sendStatus: sinon.stub()
|
||||
};
|
||||
|
||||
req = {
|
||||
query: { keep: 'keep' }
|
||||
};
|
||||
});
|
||||
|
||||
context('party', function() {
|
||||
beforeEach(function() {
|
||||
group.type = 'party';
|
||||
});
|
||||
|
||||
it('prevents user from leaving party if quest is active and part of the active members list', function() {
|
||||
group.quest = {
|
||||
active: true,
|
||||
members: {
|
||||
another_user: true,
|
||||
yet_another_user: null,
|
||||
'user-id': true
|
||||
}
|
||||
};
|
||||
|
||||
groupsController.leave(req, res);
|
||||
|
||||
expect(group.leave).to.not.be.called;
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(403, 'You cannot leave party during an active quest. Please leave the quest first.');
|
||||
});
|
||||
|
||||
it('prevents quest leader from leaving a party if they have started a quest', function() {
|
||||
group.quest = {
|
||||
active: false,
|
||||
leader: 'user-id'
|
||||
};
|
||||
|
||||
groupsController.leave(req, res);
|
||||
|
||||
expect(group.leave).to.not.be.called;
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(403, 'You cannot leave your party when you have started a quest. Abort the quest first.');
|
||||
});
|
||||
|
||||
it('leaves party if quest is not active', function() {
|
||||
group.quest = {
|
||||
active: false,
|
||||
members: {
|
||||
another_user: true,
|
||||
yet_another_user: null,
|
||||
'user-id': null
|
||||
}
|
||||
};
|
||||
|
||||
groupsController.leave(req, res);
|
||||
|
||||
expect(group.leave).to.be.calledOnce;
|
||||
expect(res.json).to.not.be.called;
|
||||
});
|
||||
|
||||
it('leaves party if quest is active, but user is not part of quest', function() {
|
||||
group.quest = {
|
||||
active: true,
|
||||
members: {
|
||||
another_user: true,
|
||||
yet_another_user: null,
|
||||
'user-id': null
|
||||
}
|
||||
};
|
||||
|
||||
groupsController.leave(req, res);
|
||||
|
||||
expect(group.leave).to.be.calledOnce;
|
||||
expect(res.json).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#questLeave', function() {
|
||||
var res, req, group, user, saveSpy;
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(Q, 'all').returns({
|
||||
done: sinon.stub().yields()
|
||||
});
|
||||
group = {
|
||||
_id: 'group-id',
|
||||
type: 'party',
|
||||
quest: {
|
||||
leader : 'another-user',
|
||||
active: true,
|
||||
members: {
|
||||
'user-id': true,
|
||||
'another-user': true
|
||||
},
|
||||
key : 'vice1',
|
||||
progress : {
|
||||
hp : 364,
|
||||
collect : {}
|
||||
}
|
||||
},
|
||||
save: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
party : {
|
||||
quest : {
|
||||
key : 'vice1',
|
||||
progress : {
|
||||
up : 50,
|
||||
down : 0,
|
||||
collectedItems : {}
|
||||
},
|
||||
completed : null,
|
||||
RSVPNeeded : false
|
||||
}
|
||||
},
|
||||
save: sinon.stub().yields(),
|
||||
markModified: sinon.spy()
|
||||
};
|
||||
|
||||
res = {
|
||||
locals: {
|
||||
group: group,
|
||||
user: user
|
||||
},
|
||||
json: sinon.stub(),
|
||||
sendStatus: sinon.stub()
|
||||
};
|
||||
|
||||
req = { };
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Promise.all.restore();
|
||||
});
|
||||
|
||||
context('error conditions', function() {
|
||||
it('errors if quest is not active', function() {
|
||||
group.quest.active = false;
|
||||
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(
|
||||
404,
|
||||
{ err: 'No active quest to leave' }
|
||||
);
|
||||
});
|
||||
|
||||
it('errors if user is not part of quest', function() {
|
||||
delete group.quest.members[user._id];
|
||||
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(
|
||||
403,
|
||||
{ err: 'You are not part of the quest' }
|
||||
);
|
||||
});
|
||||
|
||||
it('does not allow quest leader to leave quest', function() {
|
||||
group.quest.leader = 'user-id';
|
||||
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(
|
||||
403,
|
||||
{ err: 'Quest leader cannot leave quest' }
|
||||
);
|
||||
});
|
||||
|
||||
it('sends 500 if group cannot save', function() {
|
||||
Promise.all.returns({
|
||||
done: sinon.stub().callsArgWith(1, {err: 'save error'})
|
||||
});
|
||||
var nextSpy = sinon.spy();
|
||||
|
||||
groupsController.questLeave(req, res, nextSpy);
|
||||
|
||||
expect(res.json).to.not.be.called;
|
||||
expect(nextSpy).to.be.calledOnce;
|
||||
expect(nextSpy).to.be.calledWith({err: 'save error'});
|
||||
});
|
||||
});
|
||||
|
||||
context('success', function() {
|
||||
it('removes user from quest', function() {
|
||||
expect(group.quest.members[user._id]).to.exist;
|
||||
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(group.quest.members[user._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('scrubs quest data from user', function() {
|
||||
user.party.quest.progress = {
|
||||
up: 100,
|
||||
down: 32,
|
||||
collectedItems: 16,
|
||||
collect: {
|
||||
foo: 12,
|
||||
bar: 4
|
||||
}
|
||||
};
|
||||
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(user.party.quest.key).to.not.exist;
|
||||
expect(user.party.quest.progress).to.eql({
|
||||
up: 0,
|
||||
down: 0,
|
||||
collectedItems: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends back 204 on success', function() {
|
||||
groupsController.questLeave(req, res);
|
||||
|
||||
expect(res.sendStatus).to.be.calledOnce;
|
||||
expect(res.sendStatus).to.be.calledWith(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeMember', function() {
|
||||
var req, res, group, user;
|
||||
|
||||
beforeEach(function() {
|
||||
user = { _id: 'user-id' };
|
||||
group = {
|
||||
_id: 'group-id',
|
||||
leader: 'user-id',
|
||||
members: ['user-id', 'member-to-boot', 'another-user']
|
||||
}
|
||||
res = {
|
||||
locals: {
|
||||
user: user,
|
||||
group: group
|
||||
},
|
||||
sendStatus: sinon.stub()
|
||||
};
|
||||
req = {
|
||||
query: {
|
||||
uuid: 'member-to-boot'
|
||||
}
|
||||
};
|
||||
|
||||
sinon.stub(Group, 'update');
|
||||
sinon.stub(User, 'update');
|
||||
sinon.stub(User, 'findById');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Group.update.restore();
|
||||
User.update.restore();
|
||||
User.findById.restore();
|
||||
});
|
||||
|
||||
context('quest behavior', function() {
|
||||
it('removes quest from party if booted member was quest leader', function() {
|
||||
group.quest = {
|
||||
leader: 'member-to-boot',
|
||||
active: true,
|
||||
members: {
|
||||
'user-id': true,
|
||||
'leader-id': true,
|
||||
'member-to-boot': true
|
||||
},
|
||||
key: 'whale'
|
||||
}
|
||||
|
||||
groupsController.removeMember(req, res);
|
||||
|
||||
expect(Group.update).to.be.calledOnce;
|
||||
expect(Group.update).to.be.calledWith(
|
||||
{ _id: 'group-id'},
|
||||
{
|
||||
'$inc': { memberCount: -1 },
|
||||
'$pull': { members: 'member-to-boot' },
|
||||
'$set': { quest: {key: null, leader: null} }
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('returns quest scroll to booted member if booted member was leader of quest', function() {
|
||||
Group.update.yields();
|
||||
var bootedMember = {
|
||||
_id: 'member-to-boot',
|
||||
apiToken: 'api',
|
||||
preferences: {
|
||||
emailNotifications: {
|
||||
kickedGroup: false
|
||||
}
|
||||
}
|
||||
};
|
||||
User.findById.yields(null, bootedMember);
|
||||
User.update.returns({
|
||||
exec: sinon.stub()
|
||||
});
|
||||
|
||||
group.quest = {
|
||||
leader: 'member-to-boot',
|
||||
active: true,
|
||||
members: {
|
||||
'user-id': true,
|
||||
'leader-id': true,
|
||||
'member-to-boot': true
|
||||
},
|
||||
key: 'whale'
|
||||
}
|
||||
|
||||
groupsController.removeMember(req, res);
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWith(
|
||||
{ _id: 'member-to-boot', apiToken: 'api' },
|
||||
{
|
||||
'$unset': { 'newMessages.group-id': ''},
|
||||
'$inc': { 'items.quests.whale': 1 }
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,617 +0,0 @@
|
||||
var sinon = require('sinon');
|
||||
var chai = require("chai")
|
||||
chai.use(require("sinon-chai"))
|
||||
var expect = chai.expect
|
||||
var rewire = require('rewire');
|
||||
|
||||
var userController = rewire('../../../website/server/controllers/api-v2/user');
|
||||
|
||||
describe('User Controller', function() {
|
||||
|
||||
describe('score', function() {
|
||||
var req, res, user;
|
||||
|
||||
beforeEach(function() {
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
_tmp: {
|
||||
drop: true
|
||||
},
|
||||
_statsComputed: {
|
||||
maxMP: 100
|
||||
},
|
||||
ops: {
|
||||
score: sinon.stub(),
|
||||
addTask: sinon.stub()
|
||||
},
|
||||
stats: {
|
||||
lvl: 10,
|
||||
hp: 43,
|
||||
mp: 50
|
||||
},
|
||||
preferences: {
|
||||
webhooks: {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint'
|
||||
}
|
||||
}
|
||||
},
|
||||
save: sinon.stub(),
|
||||
tasks: {
|
||||
task_id: {
|
||||
id: 'task_id',
|
||||
type: 'todo'
|
||||
}
|
||||
}
|
||||
};
|
||||
req = {
|
||||
language: 'en',
|
||||
params: {
|
||||
id: 'task_id',
|
||||
direction: 'up'
|
||||
}
|
||||
};
|
||||
res = {
|
||||
locals: { user: user },
|
||||
json: sinon.spy()
|
||||
};
|
||||
});
|
||||
|
||||
context('early return conditions', function() {
|
||||
it('sends an error when no id is provided', function() {
|
||||
delete req.params.id;
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(400, {err: ':id required'});
|
||||
});
|
||||
|
||||
it('sends an error when no direction is provided', function() {
|
||||
delete req.params.direction;
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(400, {err: ":direction must be 'up' or 'down'"});
|
||||
});
|
||||
|
||||
it('calls next when direction is "unlink"', function() {
|
||||
req.params.direction = 'unlink';
|
||||
var nextSpy = sinon.spy();
|
||||
|
||||
userController.score(req, res, nextSpy);
|
||||
|
||||
expect(nextSpy).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('calls next when direction is "sort"', function() {
|
||||
req.params.direction = 'sort';
|
||||
var nextSpy = sinon.spy();
|
||||
|
||||
userController.score(req, res, nextSpy);
|
||||
|
||||
expect(nextSpy).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
context('task exists', function() {
|
||||
it('sets todo to completed if direction is "up"', function() {
|
||||
req.params.direction = 'up';
|
||||
req.params.id = 'todo_id';
|
||||
user.tasks.todo_id = {
|
||||
_id: 'todo_id',
|
||||
type: 'todo',
|
||||
completed: false
|
||||
};
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.tasks.todo_id.completed).to.eql(true);
|
||||
});
|
||||
|
||||
it('sets todo to not completed if direction is "down"', function() {
|
||||
req.params.direction = 'down';
|
||||
req.params.id = 'todo_id';
|
||||
user.tasks.todo_id = {
|
||||
_id: 'todo_id',
|
||||
type: 'todo',
|
||||
completed: true
|
||||
};
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.tasks.todo_id.completed).to.eql(false);
|
||||
});
|
||||
|
||||
it('sets daily to completed if direction is "up"', function() {
|
||||
req.params.direction = 'up';
|
||||
req.params.id = 'daily_id';
|
||||
user.tasks.daily_id = {
|
||||
_id: 'daily_id',
|
||||
type: 'daily',
|
||||
completed: false
|
||||
};
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.tasks.daily_id.completed).to.eql(true);
|
||||
});
|
||||
|
||||
it('sets daily to not completed if direction is "down"', function() {
|
||||
req.params.direction = 'down';
|
||||
req.params.id = 'daily_id';
|
||||
user.tasks.daily_id = {
|
||||
_id: 'daily_id',
|
||||
type: 'daily',
|
||||
completed: true
|
||||
};
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.tasks.daily_id.completed).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('task does not exist', function() {
|
||||
it('creates the task', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: true,
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('provides a default note if no note is provided', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'todo',
|
||||
text: 'some todo'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: true,
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('todo task is completed if direction is "up"', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.direction = 'up';
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: true,
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('todo task is not completed if direction is "down"', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.direction = 'down';
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: false,
|
||||
type: 'todo',
|
||||
text: 'some todo',
|
||||
notes: 'some notes'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('daily task is completed if direction is "up"', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.direction = 'up';
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'daily',
|
||||
text: 'some daily',
|
||||
notes: 'some notes'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: true,
|
||||
type: 'daily',
|
||||
text: 'some daily',
|
||||
notes: 'some notes'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('daily task is not completed if direction is "down"', function() {
|
||||
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
|
||||
|
||||
req.params.direction = 'down';
|
||||
req.params.id = 'an-id-that-does-not-exist-yet';
|
||||
req.body = {
|
||||
type: 'daily',
|
||||
text: 'some daily',
|
||||
notes: 'some notes'
|
||||
}
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.addTask).to.be.calledOnce;
|
||||
expect(user.ops.addTask).to.be.calledWith({
|
||||
body: {
|
||||
id: 'an-id-that-does-not-exist-yet',
|
||||
completed: false,
|
||||
type: 'daily',
|
||||
text: 'some daily',
|
||||
notes: 'some notes'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('whether task exists or it does not exist', function() {
|
||||
it('calls user.ops.score', function() {
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.ops.score).to.be.calledOnce;
|
||||
expect(user.ops.score).to.be.calledWith({
|
||||
params: {id: 'task_id', direction: 'up'},
|
||||
language: 'en'
|
||||
});
|
||||
});
|
||||
|
||||
it('saves user', function() {
|
||||
userController.score(req, res);
|
||||
|
||||
expect(user.save).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
context('user.save callback', function() {
|
||||
var savedUser;
|
||||
beforeEach(function() {
|
||||
savedUser = {
|
||||
stats: user.stats
|
||||
}
|
||||
|
||||
user.save.yields(null, savedUser);
|
||||
|
||||
user.ops.score.returns(1.5);
|
||||
});
|
||||
|
||||
it('calls next if saving yields an error', function() {
|
||||
var nextSpy = sinon.spy();
|
||||
user.save.yields('an error');
|
||||
|
||||
userController.score(req, res, nextSpy);
|
||||
|
||||
expect(nextSpy).to.be.calledOnce;
|
||||
expect(nextSpy).to.be.calledWith('an error');
|
||||
});
|
||||
|
||||
it('sends some user data with res.json', function() {
|
||||
userController.score(req, res);
|
||||
|
||||
expect(res.json).to.be.calledOnce;
|
||||
expect(res.json).to.be.calledWith(200, {
|
||||
delta: 1.5,
|
||||
_tmp: user._tmp,
|
||||
lvl: 10,
|
||||
hp: 43,
|
||||
mp: 50
|
||||
});
|
||||
});
|
||||
|
||||
it('sends webhooks', function() {
|
||||
var webhook = require('../../../website/server/libs/webhook');
|
||||
sinon.spy(webhook, 'sendTaskWebhook');
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(webhook.sendTaskWebhook).to.be.calledOnce;
|
||||
expect(webhook.sendTaskWebhook).to.be.calledWith(
|
||||
user.preferences.webhooks,
|
||||
{
|
||||
task: {
|
||||
delta: 1.5,
|
||||
details: { completed: true, id: "task_id", type: "todo" },
|
||||
direction: "up"
|
||||
},
|
||||
user: {
|
||||
_id: "user-id",
|
||||
_tmp: { drop: true },
|
||||
stats: { hp: 43, lvl: 10, maxHealth: 50, maxMP: 100, mp: 50, toNextLevel: 260 }
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('save callback dealing with non challenge tasks', function() {
|
||||
var Challenge = require('../../../website/server/models/challenge').model;
|
||||
|
||||
beforeEach(function() {
|
||||
user.save.yields(null, user);
|
||||
sinon.stub(Challenge, 'findById');
|
||||
req.params.id = 'non_active_challenge_task';
|
||||
user.tasks.non_active_challenge_task = {
|
||||
id: 'non_active_challenge_task',
|
||||
challenge: { id: 'some-id' },
|
||||
type: 'todo'
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Challenge.findById.restore();
|
||||
});
|
||||
|
||||
it('returns early if not a challenge', function() {
|
||||
delete user.tasks.non_active_challenge_task.challenge;
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.not.be.called;
|
||||
});
|
||||
|
||||
it('returns early if no challenge id', function() {
|
||||
delete user.tasks.non_active_challenge_task.challenge.id;
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.not.be.called;
|
||||
});
|
||||
|
||||
it('returns early if challenge is broken', function() {
|
||||
user.tasks.non_active_challenge_task.challenge.broken = true;
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.not.be.called;
|
||||
});
|
||||
|
||||
it('returns early if task is a reward', function() {
|
||||
user.tasks.non_active_challenge_task.type = 'reward';
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.not.be.called;
|
||||
});
|
||||
|
||||
it('calls next if there is an error looking up challenge', function() {
|
||||
Challenge.findById.yields('an error');
|
||||
var nextSpy = sinon.spy();
|
||||
|
||||
userController.score(req, res, nextSpy);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
expect(nextSpy).to.be.calledOnce;
|
||||
expect(nextSpy).to.be.calledWith('an error');
|
||||
});
|
||||
});
|
||||
|
||||
context('save callback dealing with challenge tasks', function() {
|
||||
var Challenge = require('../../../website/server/models/challenge').model;
|
||||
var chal;
|
||||
|
||||
beforeEach(function() {
|
||||
chal = {
|
||||
id: 'id',
|
||||
tasks: {
|
||||
active_challenge_task: { id: 'active_challenge_task', value: 1 }
|
||||
},
|
||||
syncToUser: sinon.spy(),
|
||||
save: sinon.spy()
|
||||
};
|
||||
user.save.yields(null, user);
|
||||
user.ops.score.returns(1.4);
|
||||
req.params.id = 'active_challenge_task';
|
||||
user.tasks.active_challenge_task = {
|
||||
id: 'active_challenge_task',
|
||||
challenge: { id: 'challenge_id' },
|
||||
type: 'todo'
|
||||
};
|
||||
|
||||
sinon.stub(Challenge, 'findById');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Challenge.findById.restore();
|
||||
});
|
||||
|
||||
xit('sets challenge as broken if no challenge can be found', function() {
|
||||
Challenge.findById.yields(null, null);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
expect(user.tasks.active_challenge_task.challenge.broken).to.eql('CHALLENGE_DELETED');
|
||||
});
|
||||
|
||||
it('notifies user if task has been deleted from challenge', function() {
|
||||
delete chal.tasks.active_challenge_task;
|
||||
Challenge.findById.yields(null, chal);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
expect(chal.syncToUser).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('changes task value by delta', function() {
|
||||
Challenge.findById.yields(null, chal);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
expect(chal.tasks.active_challenge_task.value).to.be.eql(2.4);
|
||||
});
|
||||
|
||||
it('adds history if task is a habit', function() {
|
||||
chal.tasks.active_challenge_task = {
|
||||
id: 'active_challenge_task',
|
||||
type: 'habit',
|
||||
value: 1,
|
||||
history: [{value: 1, date: 1234}]
|
||||
};
|
||||
|
||||
Challenge.findById.yields(null, chal);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
|
||||
var historyEvent = chal.tasks.active_challenge_task.history[1];
|
||||
|
||||
expect(historyEvent.value).to.eql(2.4);
|
||||
expect(historyEvent.date).to.be.closeTo(+new Date, 10);
|
||||
});
|
||||
|
||||
it('adds history if task is a daily', function() {
|
||||
chal.tasks.active_challenge_task = {
|
||||
id: 'active_challenge_task',
|
||||
type: 'daily',
|
||||
value: 1,
|
||||
history: [{value: 1, date: 1234}]
|
||||
};
|
||||
|
||||
Challenge.findById.yields(null, chal);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
|
||||
var historyEvent = chal.tasks.active_challenge_task.history[1];
|
||||
|
||||
expect(historyEvent.value).to.eql(2.4);
|
||||
expect(historyEvent.date).to.be.closeTo(+new Date, 10);
|
||||
});
|
||||
|
||||
it('saves the challenge data', function() {
|
||||
Challenge.findById.yields(null, chal);
|
||||
|
||||
userController.score(req, res);
|
||||
|
||||
expect(Challenge.findById).to.be.calledOnce;
|
||||
expect(chal.save).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addTenGems', function() {
|
||||
var req, res, user;
|
||||
|
||||
beforeEach(function() {
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
balance: 5,
|
||||
save: sinon.stub().yields()
|
||||
};
|
||||
req = { };
|
||||
res = {
|
||||
locals: { user: user },
|
||||
send: sinon.spy()
|
||||
};
|
||||
});
|
||||
|
||||
it('adds 2.5 to user balance', function() {
|
||||
userController.addTenGems(req, res);
|
||||
|
||||
expect(user.balance).to.eql(7.5);
|
||||
expect(user.save).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('sends back 204', function() {
|
||||
userController.addTenGems(req, res);
|
||||
|
||||
expect(res.sendStatus).to.be.calledOnce;
|
||||
expect(res.sendStatus).to.be.calledWith(204);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addHourglass', function() {
|
||||
var req, res, user;
|
||||
|
||||
beforeEach(function() {
|
||||
user = {
|
||||
_id: 'user-id',
|
||||
purchased: { plan: { consecutive: { trinkets: 3 } } },
|
||||
save: sinon.stub().yields()
|
||||
};
|
||||
req = { };
|
||||
res = {
|
||||
locals: { user: user },
|
||||
send: sinon.spy()
|
||||
};
|
||||
});
|
||||
|
||||
it('adds an hourglass to user', function() {
|
||||
userController.addHourglass(req, res);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.save).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('sends back 204', function() {
|
||||
userController.addHourglass(req, res);
|
||||
|
||||
expect(res.sendStatus).to.be.calledOnce;
|
||||
expect(res.sendStatus).to.be.calledWith(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
var sinon = require('sinon');
|
||||
var chai = require("chai")
|
||||
chai.use(require("sinon-chai"))
|
||||
var expect = chai.expect
|
||||
var rewire = require('rewire');
|
||||
|
||||
var webhook = rewire('../../website/server/libs/api-v2/webhook');
|
||||
|
||||
describe('webhooks', function() {
|
||||
var postSpy;
|
||||
|
||||
beforeEach(function() {
|
||||
postSpy = sinon.stub();
|
||||
webhook.__set__('request.post', postSpy);
|
||||
});
|
||||
|
||||
describe('sendTaskWebhook', function() {
|
||||
var task = {
|
||||
details: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
direction: 'up'
|
||||
};
|
||||
|
||||
var data = {
|
||||
task: task,
|
||||
user: { _id: 'user-id' }
|
||||
};
|
||||
|
||||
it('does not send if no webhook endpoints exist', function() {
|
||||
var webhooks = { };
|
||||
|
||||
webhook.sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(postSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if no webhooks are enabled', function() {
|
||||
var webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: false,
|
||||
url: 'http://example.org/endpoint'
|
||||
}
|
||||
};
|
||||
|
||||
webhook.sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(postSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if webhook url is not valid', function() {
|
||||
var webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://malformedurl/endpoint'
|
||||
}
|
||||
};
|
||||
|
||||
webhook.sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(postSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('sends task direction, task, task delta, and abridged user data', function() {
|
||||
var webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint'
|
||||
}
|
||||
};
|
||||
|
||||
webhook.sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(postSpy).to.be.calledOnce;
|
||||
expect(postSpy).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id'
|
||||
}
|
||||
},
|
||||
json: true
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a post request for each webhook endpoint', function() {
|
||||
var webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint'
|
||||
},
|
||||
'second-webhook': {
|
||||
sort: 1,
|
||||
id: 'second-webhook',
|
||||
enabled: true,
|
||||
url: 'http://example.com/2/endpoint'
|
||||
}
|
||||
};
|
||||
|
||||
webhook.sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(postSpy).to.be.calledTwice;
|
||||
expect(postSpy).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id'
|
||||
}
|
||||
},
|
||||
json: true
|
||||
});
|
||||
expect(postSpy).to.be.calledWith({
|
||||
url: 'http://example.com/2/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id'
|
||||
}
|
||||
},
|
||||
json: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ var baseConfig = {
|
||||
extensions: ['', '.js', '.vue'],
|
||||
fallback: [path.join(__dirname, '../node_modules')],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, '../website/client'),
|
||||
client: path.resolve(__dirname, '../website/client'),
|
||||
assets: path.resolve(__dirname, '../website/client/assets'),
|
||||
components: path.resolve(__dirname, '../website/client/components'),
|
||||
},
|
||||
|
||||
@@ -36,8 +36,13 @@
|
||||
background-color: #727272;
|
||||
}
|
||||
|
||||
/* FIXME figure out how to handle customize menu!! */
|
||||
/*.customize-menu .f_head_0 {width: 60px; height: 60px; background-position: -1917px -9px;}*/
|
||||
/* FIXME figure out how to handle customize menu!!
|
||||
.customize-menu .f_head_0 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-position: -1917px -9px;
|
||||
}
|
||||
*/
|
||||
|
||||
.achievement {
|
||||
float:left;
|
||||
@@ -51,7 +56,8 @@
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"], [class*="Mount_Body_"]{
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,30 @@
|
||||
.2014_Fall_HealerPROMO2 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1212px -1488px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.2014_Fall_Mage_PROMO9 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -884px;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
.2014_Fall_RoguePROMO3 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -558px -1385px;
|
||||
width: 105px;
|
||||
height: 90px;
|
||||
}
|
||||
.2014_Fall_Warrior_PROMO {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -848px -1488px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_android {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -978px -573px;
|
||||
background-position: -1095px -151px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201602 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1417px -103px;
|
||||
background-position: -1418px -103px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201603 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -103px;
|
||||
background-position: -1276px -103px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201604 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -442px;
|
||||
background-position: -593px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201605 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px 0px;
|
||||
background-position: -281px -525px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
@@ -66,127 +42,139 @@
|
||||
}
|
||||
.promo_backgrounds_armoire_201608 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -442px;
|
||||
background-position: -734px 0px;
|
||||
width: 140px;
|
||||
height: 439px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201609 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px 0px;
|
||||
background-position: -734px -440px;
|
||||
width: 139px;
|
||||
height: 438px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201610 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backtoschool {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -151px -1488px;
|
||||
background-position: -1523px -860px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_burnout {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -151px;
|
||||
background-position: -875px -151px;
|
||||
width: 219px;
|
||||
height: 240px;
|
||||
}
|
||||
.promo_chairs_glasses {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -220px;
|
||||
background-position: -1706px 0px;
|
||||
width: 51px;
|
||||
height: 210px;
|
||||
}
|
||||
.promo_classes_fall_2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -1013px;
|
||||
background-position: -326px -1210px;
|
||||
width: 321px;
|
||||
height: 100px;
|
||||
}
|
||||
.promo_classes_fall_2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -878px;
|
||||
background-position: -1276px -1026px;
|
||||
width: 377px;
|
||||
height: 99px;
|
||||
}
|
||||
.promo_classes_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -573px;
|
||||
background-position: -875px -676px;
|
||||
width: 103px;
|
||||
height: 348px;
|
||||
}
|
||||
.promo_contrib_spotlight_beffymaroo {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1154px -573px;
|
||||
background-position: -1155px -676px;
|
||||
width: 114px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_contrib_spotlight_cantras {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1575px;
|
||||
width: 87px;
|
||||
height: 109px;
|
||||
}
|
||||
.promo_cow {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -281px -525px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_dilatoryDistress {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1316px -1385px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_egg_mounts {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -398px;
|
||||
width: 280px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_enchanted_armoire {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -499px -1114px;
|
||||
width: 374px;
|
||||
height: 76px;
|
||||
}
|
||||
.promo_enchanted_armoire_201507 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -289px -1294px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201508 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -725px -1294px;
|
||||
width: 180px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201509 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -666px -1488px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201511 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -978px;
|
||||
width: 122px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201601 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -939px -1488px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_floral_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1559px -103px;
|
||||
width: 105px;
|
||||
height: 273px;
|
||||
}
|
||||
.promo_ghost_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_dilatoryDistress {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1758px -1401px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_egg_mounts {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1276px -398px;
|
||||
width: 280px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_enchanted_armoire {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1276px -1126px;
|
||||
width: 374px;
|
||||
height: 76px;
|
||||
}
|
||||
.promo_enchanted_armoire_201507 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1155px -1210px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201508 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1373px -1210px;
|
||||
width: 180px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201509 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1758px -946px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201511 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -1022px;
|
||||
width: 122px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201601 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1758px -309px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_floral_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -220px;
|
||||
width: 105px;
|
||||
height: 273px;
|
||||
}
|
||||
.promo_ghost_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_habitica {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1094px -151px;
|
||||
background-position: -979px -676px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
@@ -198,295 +186,325 @@
|
||||
}
|
||||
.promo_haunted_hair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -734px;
|
||||
background-position: -452px -884px;
|
||||
width: 100px;
|
||||
height: 137px;
|
||||
}
|
||||
.promo_item_notif {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1385px;
|
||||
background-position: 0px -1321px;
|
||||
width: 249px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_mystery_201405 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1589px -1385px;
|
||||
background-position: -1758px -1310px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201406 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -358px -326px;
|
||||
background-position: -1758px -212px;
|
||||
width: 90px;
|
||||
height: 96px;
|
||||
}
|
||||
.promo_mystery_201407 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1064px -1114px;
|
||||
background-position: -1706px -412px;
|
||||
width: 42px;
|
||||
height: 62px;
|
||||
}
|
||||
.promo_mystery_201408 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1196px -1013px;
|
||||
background-position: -593px -1032px;
|
||||
width: 60px;
|
||||
height: 71px;
|
||||
}
|
||||
.promo_mystery_201409 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1030px -1488px;
|
||||
background-position: -1758px -1492px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201410 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1131px -840px;
|
||||
background-position: -1203px -943px;
|
||||
width: 72px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201411 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1225px -1385px;
|
||||
background-position: -1758px -1128px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201412 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1021px -1114px;
|
||||
background-position: -1706px -345px;
|
||||
width: 42px;
|
||||
height: 66px;
|
||||
}
|
||||
.promo_mystery_201501 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1653px -878px;
|
||||
background-position: -1706px -211px;
|
||||
width: 48px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201502 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -393px -1488px;
|
||||
background-position: -1758px -764px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201503 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -575px -1488px;
|
||||
background-position: -1758px -673px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201504 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -1030px;
|
||||
background-position: -654px -1032px;
|
||||
width: 60px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201505 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -757px -1488px;
|
||||
background-position: -1758px -491px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201506 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -654px -1030px;
|
||||
background-position: -1706px -275px;
|
||||
width: 42px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201507 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1601px -1081px;
|
||||
background-position: -1758px 0px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201508 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -664px -1385px;
|
||||
background-position: -631px -1424px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201509 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1121px -1488px;
|
||||
background-position: -1758px -400px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201510 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -758px -1385px;
|
||||
background-position: -1007px -1424px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201511 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1134px -1385px;
|
||||
background-position: -1758px -1583px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201512 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1638px -978px;
|
||||
background-position: -1206px -1025px;
|
||||
width: 60px;
|
||||
height: 81px;
|
||||
}
|
||||
.promo_mystery_201601 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -975px;
|
||||
background-position: -449px -1321px;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201602 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1407px -1385px;
|
||||
background-position: -1758px -1219px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201603 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1498px -1385px;
|
||||
background-position: -1758px -582px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201604 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -852px -1385px;
|
||||
background-position: -725px -1424px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201605 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -302px -1488px;
|
||||
background-position: -1758px -1037px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201606 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -358px -220px;
|
||||
background-position: -1758px -106px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201607 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -484px -1488px;
|
||||
background-position: -1758px -855px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201608 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1040px -1385px;
|
||||
background-position: -913px -1424px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201609 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -946px -1385px;
|
||||
background-position: -819px -1424px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201610 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -685px -1321px;
|
||||
width: 63px;
|
||||
height: 84px;
|
||||
}
|
||||
.promo_mystery_3014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -507px -1294px;
|
||||
background-position: -937px -1210px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_orca {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -872px;
|
||||
background-position: -416px -1424px;
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_partyhats {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1094px -327px;
|
||||
background-position: -1095px -327px;
|
||||
width: 115px;
|
||||
height: 47px;
|
||||
}
|
||||
.promo_pastel_skin {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -331px -1210px;
|
||||
background-position: -875px -1025px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.customize-option.promo_pastel_skin {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -356px -1225px;
|
||||
background-position: -900px -1040px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_peppermint_flame {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -882px;
|
||||
background-position: -1557px -398px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pet_skins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1556px -398px;
|
||||
background-position: -1556px -546px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_pet_skins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1581px -413px;
|
||||
background-position: -1581px -561px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_pyromancer {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -302px -1424px;
|
||||
width: 113px;
|
||||
height: 113px;
|
||||
}
|
||||
.promo_rainbow_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -88px -1575px;
|
||||
width: 92px;
|
||||
height: 103px;
|
||||
}
|
||||
.promo_seasonal_shop_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1276px -546px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_shimmer_hair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1210px;
|
||||
background-position: -860px -1114px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.promo_splashyskins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -250px -1385px;
|
||||
background-position: -250px -1321px;
|
||||
width: 198px;
|
||||
height: 91px;
|
||||
}
|
||||
.customize-option.promo_splashyskins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -275px -1400px;
|
||||
background-position: -275px -1336px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_spooky_sparkles_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1560px -103px;
|
||||
width: 140px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_spring_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -978px;
|
||||
background-position: -875px -573px;
|
||||
width: 362px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_springclasses2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1294px;
|
||||
background-position: -979px -852px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_springclasses2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -978px -749px;
|
||||
background-position: -648px -1210px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_staff_spotlight_Lemoness {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -439px;
|
||||
background-position: -734px -879px;
|
||||
width: 102px;
|
||||
height: 146px;
|
||||
}
|
||||
.promo_staff_spotlight_Viirus {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1145px -392px;
|
||||
background-position: -1146px -392px;
|
||||
width: 119px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_staff_spotlight_paglias {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -586px;
|
||||
background-position: -593px -884px;
|
||||
width: 99px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_summer_classes_2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px 0px;
|
||||
background-position: -1276px 0px;
|
||||
width: 429px;
|
||||
height: 102px;
|
||||
}
|
||||
@@ -498,55 +516,61 @@
|
||||
}
|
||||
.promo_summer_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px 0px;
|
||||
background-position: -875px 0px;
|
||||
width: 400px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_takeThis_gear {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -431px;
|
||||
background-position: -734px -1026px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_takethis_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -906px -1294px;
|
||||
background-position: -570px -1321px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1204px -840px;
|
||||
background-position: -1191px -1114px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_unconventional_armor2 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1132px -943px;
|
||||
width: 70px;
|
||||
height: 74px;
|
||||
}
|
||||
.promo_updos {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1522px -546px;
|
||||
background-position: -1523px -694px;
|
||||
width: 156px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_veteran_pets {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -1114px;
|
||||
background-position: -1554px -1210px;
|
||||
width: 146px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_winter_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -922px;
|
||||
background-position: -499px -1114px;
|
||||
width: 360px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_winterclasses2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -1081px;
|
||||
background-position: 0px -1210px;
|
||||
width: 325px;
|
||||
height: 110px;
|
||||
}
|
||||
.promo_winteryhair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -978px -840px;
|
||||
background-position: -979px -943px;
|
||||
width: 152px;
|
||||
height: 75px;
|
||||
}
|
||||
@@ -558,7 +582,7 @@
|
||||
}
|
||||
.npc_viirus {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -449px -1385px;
|
||||
background-position: -522px -1424px;
|
||||
width: 108px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -570,31 +594,31 @@
|
||||
}
|
||||
.scene_coding {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1488px;
|
||||
background-position: -151px -1424px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.scene_phone_peek {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1522px -712px;
|
||||
background-position: 0px -1424px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.welcome_basic_avatars {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -546px;
|
||||
background-position: -1276px -694px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
.welcome_promo_party {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -392px;
|
||||
background-position: -875px -392px;
|
||||
width: 270px;
|
||||
height: 180px;
|
||||
}
|
||||
.welcome_sample_tasks {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -712px;
|
||||
background-position: -1276px -860px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 692 KiB After Width: | Height: | Size: 729 KiB |
962
website/assets/sprites/dist/spritesmith-main-0.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-0.png
vendored
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 438 KiB |
1948
website/assets/sprites/dist/spritesmith-main-1.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-1.png
vendored
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
988
website/assets/sprites/dist/spritesmith-main-10.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-10.png
vendored
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 140 KiB |
1460
website/assets/sprites/dist/spritesmith-main-11.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-11.png
vendored
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 152 KiB |
1596
website/assets/sprites/dist/spritesmith-main-12.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-12.png
vendored
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 169 KiB |
2508
website/assets/sprites/dist/spritesmith-main-13.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-13.png
vendored
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
612
website/assets/sprites/dist/spritesmith-main-14.css
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
.Pet-Spider-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Spider-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Spider-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TRex-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Tiger-Veteran {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Floral {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Ghost {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Peppermint {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Spooky {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Thunderstorm {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Treeling-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turkey-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turkey-Gilded {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Turtle-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Unicorn-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -82px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -164px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -246px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -328px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Floral {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -410px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Ghost {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -492px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -574px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Peppermint {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -656px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -738px -700px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Spooky {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Thunderstorm {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Veteran {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -500px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -600px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet_HatchingPotion_Base {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: 0px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyBlue {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -294px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyPink {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -49px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Desert {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -98px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Floral {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -147px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ghost {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -196px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Golden {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -245px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Peppermint {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -820px -700px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Red {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -343px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shade {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -392px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Skeleton {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -441px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Spooky {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -490px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Thunderstorm {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -539px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_White {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -588px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||
.Pet_HatchingPotion_Zombie {
|
||||
background-image: url(/spritesmith-main-14.png);
|
||||
background-position: -637px -800px;
|
||||
width: 48px;
|
||||
height: 51px;
|
||||
}
|
||||