mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-27 19:22:55 +01:00
Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f38c61c70 | ||
|
|
1c018cedb1 | ||
|
|
80892bd6a8 | ||
|
|
f5ba636579 | ||
|
|
4dd7e49552 | ||
|
|
d2f673ef1e | ||
|
|
e198dd551a | ||
|
|
0bfc9d9516 | ||
|
|
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 | ||
|
|
941000d737 | ||
|
|
63ce7c6034 | ||
|
|
921f9a65a3 | ||
|
|
d6bf30eff8 | ||
|
|
faed0dff20 | ||
|
|
7bb2f4a3fa | ||
|
|
e3bcea4077 | ||
|
|
51ffe2c8c2 | ||
|
|
efc0469bef | ||
|
|
bda0617a23 | ||
|
|
913cb16638 | ||
|
|
331993c1df | ||
|
|
136e2de125 | ||
|
|
966a50431f | ||
|
|
4df1601718 | ||
|
|
4d5b6992be | ||
|
|
b54441a637 | ||
|
|
bccdf4e989 | ||
|
|
633da7ff73 | ||
|
|
d3371e323e | ||
|
|
5480157977 | ||
|
|
c5888e3d21 | ||
|
|
2ca185474f | ||
|
|
5f0c1687b5 |
@@ -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
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
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
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
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.1.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']);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ gulp.task('build:common', () => {
|
||||
|
||||
gulp.task('build:server', ['build:src', 'build:common']);
|
||||
|
||||
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (done) => {
|
||||
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
|
||||
gulp.start('grunt-build:dev', done);
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
|
||||
gulp.watch(['website/client-old/**/*.styl', 'website/common/script/*']);
|
||||
});
|
||||
|
||||
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff'], (done) => {
|
||||
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
|
||||
runSequence(
|
||||
'grunt-build:prod',
|
||||
'apidoc',
|
||||
|
||||
43
gulp/gulp-semanticui.js
Normal file
43
gulp/gulp-semanticui.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// Make semantic-ui-less work with a theme in a different folder
|
||||
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
|
||||
|
||||
// Relative to node_modules/semantic-ui-less
|
||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
|
||||
|
||||
// fix well known bug with default distribution
|
||||
function fixFontPath (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filename, 'utf8', (err, content) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
let newContent = content.replace(
|
||||
'@fontPath : \'../../themes/',
|
||||
'@fontPath : \'../../../themes/'
|
||||
);
|
||||
|
||||
fs.writeFile(filename, newContent, 'utf8', (err1) => {
|
||||
if (err) return reject(err1);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('semantic-ui', (done) => {
|
||||
// relocate default config
|
||||
fs.writeFile(
|
||||
'node_modules/semantic-ui-less/theme.config',
|
||||
`@import '${SEMANTIC_THEME_PATH}';\n`,
|
||||
'utf8',
|
||||
(err) => {
|
||||
if (err) return done(err);
|
||||
|
||||
fixFontPath('node_modules/semantic-ui-less/themes/default/globals/site.variables')
|
||||
.then(() => done())
|
||||
.catch(done);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
require('babel-register');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
require('./gulp/gulp-semanticui');
|
||||
require('./gulp/gulp-apidoc');
|
||||
require('./gulp/gulp-newstuff');
|
||||
require('./gulp/gulp-build');
|
||||
|
||||
116
migrations/20161002_add_missing_webhook_type.js
Normal file
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
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);
|
||||
}
|
||||
|
||||
86
migrations/20161030-jackolanterns.js
Normal file
86
migrations/20161030-jackolanterns.js
Normal file
@@ -0,0 +1,86 @@
|
||||
var migrationName = '20161030-jackolanterns.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* set the newStuff flag in all user accounts so they see a Bailey message
|
||||
*/
|
||||
|
||||
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 = {
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-10-01')} // remove when running migration a second time
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'migration': 1,
|
||||
'items.pets.JackOLantern-Base': 1,
|
||||
'items.mounts.JackOLantern-Base': 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 = {};
|
||||
var inc = {};
|
||||
if (user.migration !== migrationName) {
|
||||
if (user.items.mounts['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost':5};
|
||||
} else if (user.items.pets['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base':true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Base':5};
|
||||
}
|
||||
inc = {
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set, $inc:inc});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
1046
npm-shrinkwrap.json
generated
1046
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
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.0",
|
||||
"version": "3.52.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
@@ -13,9 +13,10 @@
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"babel-loader": "^6.0.0",
|
||||
"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",
|
||||
@@ -65,8 +66,11 @@
|
||||
"jade": "~1.11.0",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"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",
|
||||
@@ -83,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",
|
||||
@@ -90,10 +95,11 @@
|
||||
"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",
|
||||
"semantic-ui-less": "~2.2.4",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"shelljs": "^0.6.0",
|
||||
"stripe": "^4.2.0",
|
||||
@@ -108,9 +114,8 @@
|
||||
"vue": "^2.0.0-rc.6",
|
||||
"vue-hot-reload-api": "^1.2.0",
|
||||
"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",
|
||||
@@ -124,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",
|
||||
@@ -142,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",
|
||||
@@ -198,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';
|
||||
|
||||
@@ -73,6 +74,7 @@ describe('PUT /tasks/:id', () => {
|
||||
checklist: [
|
||||
{text: 123, completed: false},
|
||||
],
|
||||
collapseChecklist: false,
|
||||
});
|
||||
await sleep(2);
|
||||
|
||||
@@ -110,6 +112,7 @@ describe('PUT /tasks/:id', () => {
|
||||
{text: 123, completed: false},
|
||||
{text: 456, completed: true},
|
||||
],
|
||||
collapseChecklist: true,
|
||||
notes: 'new notes',
|
||||
attribute: 'per',
|
||||
tags: [challengeUserTaskId],
|
||||
@@ -142,6 +145,83 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(savedChallengeUserTask.streak).to.equal(25);
|
||||
expect(savedChallengeUserTask.reminders.length).to.equal(2);
|
||||
expect(savedChallengeUserTask.checklist.length).to.equal(2);
|
||||
expect(savedChallengeUserTask.alias).to.equal('a-short-task-name');
|
||||
expect(savedChallengeUserTask.collapseChecklist).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
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
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'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,10 @@ describe('analyticsService', () => {
|
||||
sandbox.stub(Visitor.prototype, 'transaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType, data;
|
||||
|
||||
@@ -273,6 +277,7 @@ describe('analyticsService', () => {
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}],
|
||||
balance: 12,
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
@@ -296,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', () => {
|
||||
|
||||
@@ -53,31 +53,32 @@ describe('emails', () => {
|
||||
let pathToEmailLib = '../../../../../website/server/libs/email';
|
||||
|
||||
describe('sendEmail', () => {
|
||||
it('can send an email using the default transport', () => {
|
||||
let sendMailSpy = sandbox.stub().returns(defer().promise);
|
||||
let sendMailSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
sendMailSpy = sandbox.stub().returns(defer().promise);
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendMailSpy,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('can send an email using the default transport', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
attachEmail.send();
|
||||
expect(sendMailSpy).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('logs errors', (done) => {
|
||||
let deferred = defer();
|
||||
let sendMailSpy = sandbox.stub().returns(deferred.promise);
|
||||
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendMailSpy,
|
||||
});
|
||||
sandbox.stub(logger, 'error');
|
||||
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
attachEmail.send();
|
||||
expect(sendMailSpy).to.be.calledOnce;
|
||||
deferred.reject();
|
||||
defer().reject();
|
||||
|
||||
// wait for unhandledRejection event to fire
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -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
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
5
test/client/.babelrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"comments": false
|
||||
}
|
||||
@@ -6,6 +6,6 @@ require('babel-polyfill');
|
||||
var testsContext = require.context('./specs', true, /\.spec$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
||||
// require all src files except main.js/ README.md / index.html for coverage.
|
||||
var srcContext = require.context('../../../website/client', true, /^\.\/(?!(main(\.js)?)|(index(\.html)?)$)/);
|
||||
// require all .vue and .js files except main.js for coverage.
|
||||
var srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
|
||||
srcContext.keys().forEach(srcContext);
|
||||
@@ -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
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();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,21 +16,20 @@ describe('common.fns.randomDrop', () => {
|
||||
user = generateUser();
|
||||
user._tmp = user._tmp ? user._tmp : {};
|
||||
task = generateTodo({ userId: user._id });
|
||||
predictableRandom = () => {
|
||||
return 0.5;
|
||||
};
|
||||
predictableRandom = sandbox.stub().returns(0.5);
|
||||
});
|
||||
|
||||
it('drops an item for the user.party.quest.progress', () => {
|
||||
expect(user.party.quest.progress.collectedItems).to.eql(0);
|
||||
user.party.quest.key = 'vice2';
|
||||
predictableRandom = () => {
|
||||
return 0.0001;
|
||||
};
|
||||
predictableRandom.returns(0.0001);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user.party.quest.progress.collectedItems).to.eql(1);
|
||||
expect(user._tmp.quest.collection).to.eql(1);
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user.party.quest.progress.collectedItems).to.eql(2);
|
||||
expect(user._tmp.quest.collection).to.eql(1);
|
||||
});
|
||||
|
||||
context('drops enabled', () => {
|
||||
@@ -42,15 +41,14 @@ describe('common.fns.randomDrop', () => {
|
||||
it('does nothing if user.items.lastDrop.count is exceeded', () => {
|
||||
user.items.lastDrop.count = 100;
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp).to.eql({});
|
||||
expect(user._tmp.drop).to.be.undefined;
|
||||
});
|
||||
|
||||
it('drops something when the task is a todo', () => {
|
||||
expect(user._tmp).to.eql({});
|
||||
user.flags.dropsEnabled = true;
|
||||
predictableRandom = () => {
|
||||
return 0.1;
|
||||
};
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp).to.not.eql({});
|
||||
});
|
||||
@@ -59,9 +57,8 @@ describe('common.fns.randomDrop', () => {
|
||||
task = generateHabit({ userId: user._id });
|
||||
expect(user._tmp).to.eql({});
|
||||
user.flags.dropsEnabled = true;
|
||||
predictableRandom = () => {
|
||||
return 0.1;
|
||||
};
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp).to.not.eql({});
|
||||
});
|
||||
@@ -70,9 +67,8 @@ describe('common.fns.randomDrop', () => {
|
||||
task = generateDaily({ userId: user._id });
|
||||
expect(user._tmp).to.eql({});
|
||||
user.flags.dropsEnabled = true;
|
||||
predictableRandom = () => {
|
||||
return 0.1;
|
||||
};
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp).to.not.eql({});
|
||||
});
|
||||
@@ -81,34 +77,30 @@ describe('common.fns.randomDrop', () => {
|
||||
task = generateReward({ userId: user._id });
|
||||
expect(user._tmp).to.eql({});
|
||||
user.flags.dropsEnabled = true;
|
||||
predictableRandom = () => {
|
||||
return 0.1;
|
||||
};
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp).to.not.eql({});
|
||||
});
|
||||
|
||||
it('drops food', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.65;
|
||||
};
|
||||
predictableRandom.returns(0.65);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('Food');
|
||||
});
|
||||
|
||||
it('drops eggs', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.35;
|
||||
};
|
||||
predictableRandom.returns(0.35);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('Egg');
|
||||
});
|
||||
|
||||
context('drops hatching potion', () => {
|
||||
it('drops a very rare potion', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.01;
|
||||
};
|
||||
predictableRandom.returns(0.01);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('HatchingPotion');
|
||||
expect(user._tmp.drop.value).to.eql(5);
|
||||
@@ -116,9 +108,8 @@ describe('common.fns.randomDrop', () => {
|
||||
});
|
||||
|
||||
it('drops a rare potion', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.08;
|
||||
};
|
||||
predictableRandom.returns(0.08);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('HatchingPotion');
|
||||
expect(user._tmp.drop.value).to.eql(4);
|
||||
@@ -127,9 +118,8 @@ describe('common.fns.randomDrop', () => {
|
||||
});
|
||||
|
||||
it('drops an uncommon potion', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.17;
|
||||
};
|
||||
predictableRandom.returns(0.17);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('HatchingPotion');
|
||||
expect(user._tmp.drop.value).to.eql(3);
|
||||
@@ -138,9 +128,8 @@ describe('common.fns.randomDrop', () => {
|
||||
});
|
||||
|
||||
it('drops a common potion', () => {
|
||||
predictableRandom = () => {
|
||||
return 0.20;
|
||||
};
|
||||
predictableRandom.returns(0.20);
|
||||
|
||||
randomDrop(user, { task, predictableRandom });
|
||||
expect(user._tmp.drop.type).to.eql('HatchingPotion');
|
||||
expect(user._tmp.drop.value).to.eql(2);
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import randomVal from '../../../website/common/script/fns/randomVal';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('shared.fns.randomVal', () => {
|
||||
let user;
|
||||
let obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
describe('returns a random property value from an object', () => {
|
||||
it('returns the same value when the seed is the same', () => {
|
||||
let val1 = randomVal(user, obj, {
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
let val2 = randomVal(user, obj, {
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
expect(val2).to.equal(val1);
|
||||
});
|
||||
|
||||
it('returns the same value when user.stats is the same', () => {
|
||||
user.stats.gp = 34;
|
||||
let val1 = randomVal(user, obj);
|
||||
let val2 = randomVal(user, obj);
|
||||
|
||||
expect(val2).to.equal(val1);
|
||||
});
|
||||
|
||||
it('returns a different value when the seed is different', () => {
|
||||
let val1 = randomVal(user, obj, {
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
let val2 = randomVal(user, obj, {
|
||||
seed: 333,
|
||||
});
|
||||
|
||||
expect(val2).to.not.equal(val1);
|
||||
});
|
||||
|
||||
it('returns a different value when user.stats is different', () => {
|
||||
user.stats.gp = 34;
|
||||
let val1 = randomVal(user, obj);
|
||||
user.stats.gp = 343;
|
||||
let val2 = randomVal(user, obj);
|
||||
|
||||
expect(val2).to.not.equal(val1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns a random key from an object', () => {
|
||||
it('returns the same key when the seed is the same', () => {
|
||||
let key1 = randomVal(user, obj, {
|
||||
key: true,
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
let key2 = randomVal(user, obj, {
|
||||
key: true,
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
expect(key2).to.equal(key1);
|
||||
});
|
||||
|
||||
it('returns the same key when user.stats is the same', () => {
|
||||
user.stats.gp = 45;
|
||||
let key1 = randomVal(user, obj, {
|
||||
key: true,
|
||||
});
|
||||
|
||||
let key2 = randomVal(user, obj, {
|
||||
key: true,
|
||||
});
|
||||
|
||||
expect(key2).to.equal(key1);
|
||||
});
|
||||
|
||||
it('returns a different key when the seed is different', () => {
|
||||
let key1 = randomVal(user, obj, {
|
||||
key: true,
|
||||
seed: 222,
|
||||
});
|
||||
|
||||
let key2 = randomVal(user, obj, {
|
||||
key: true,
|
||||
seed: 333,
|
||||
});
|
||||
|
||||
expect(key2).to.not.equal(key1);
|
||||
});
|
||||
|
||||
it('returns a different key when user.stats is different', () => {
|
||||
user.stats.gp = 45;
|
||||
let key1 = randomVal(user, obj, {
|
||||
key: true,
|
||||
});
|
||||
|
||||
user.stats.gp = 43;
|
||||
|
||||
let key2 = randomVal(user, obj, {
|
||||
key: true,
|
||||
});
|
||||
|
||||
expect(key2).to.not.equal(key1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,18 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import count from '../../../website/common/script/count';
|
||||
import buyArmoire from '../../../website/common/script/ops/buyArmoire';
|
||||
import shared from '../../../website/common/script';
|
||||
import randomVal from '../../../website/common/script/libs/randomVal';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyArmoire', () => {
|
||||
let user;
|
||||
let YIELD_EQUIPMENT = 0.5;
|
||||
let YIELD_FOOD = 0.7;
|
||||
let YIELD_EXP = 0.9;
|
||||
|
||||
function getFullArmoire () {
|
||||
let fullArmoire = {};
|
||||
|
||||
_(content.gearTypes).each((type) => {
|
||||
@@ -29,39 +23,36 @@ describe('shared.ops.buyArmoire', () => {
|
||||
}).value();
|
||||
}).value();
|
||||
|
||||
return fullArmoire;
|
||||
}
|
||||
|
||||
describe('shared.ops.buyArmoire', () => {
|
||||
let user;
|
||||
let YIELD_EQUIPMENT = 0.5;
|
||||
let YIELD_FOOD = 0.7;
|
||||
let YIELD_EXP = 0.9;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
|
||||
user.items.gear.owned = {
|
||||
weapon_warrior_0: true,
|
||||
};
|
||||
user.achievements.ultimateGearSets = { rogue: true };
|
||||
user.flags.armoireOpened = true;
|
||||
user.stats.exp = 0;
|
||||
user.items.food = {};
|
||||
|
||||
sinon.stub(shared.fns, 'randomVal');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sandbox.stub(randomVal, 'trueRandom');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.fns.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
randomVal.trueRandom.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
it('does not open if user does not have enough gold', (done) => {
|
||||
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
|
||||
user.stats.gp = 50;
|
||||
|
||||
try {
|
||||
@@ -71,13 +62,6 @@ describe('shared.ops.buyArmoire', () => {
|
||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
expect(user.items.food).to.be.empty;
|
||||
expect(user.stats.exp).to.eql(0);
|
||||
@@ -86,7 +70,6 @@ describe('shared.ops.buyArmoire', () => {
|
||||
});
|
||||
|
||||
it('does not open without Ultimate Gear achievement', (done) => {
|
||||
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
|
||||
user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false};
|
||||
|
||||
try {
|
||||
@@ -96,13 +79,6 @@ describe('shared.ops.buyArmoire', () => {
|
||||
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
expect(user.items.food).to.be.empty;
|
||||
expect(user.stats.exp).to.eql(0);
|
||||
@@ -112,93 +88,83 @@ describe('shared.ops.buyArmoire', () => {
|
||||
});
|
||||
|
||||
context('non-gear awards', () => {
|
||||
// Skipped because can't stub predictableRandom correctly
|
||||
xit('gives Experience', () => {
|
||||
shared.fns.predictableRandom.returns(YIELD_EXP);
|
||||
it('gives Experience', () => {
|
||||
let previousExp = user.stats.exp;
|
||||
randomVal.trueRandom.returns(YIELD_EXP);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
|
||||
expect(user.items.food).to.be.empty;
|
||||
expect(user.stats.exp).to.eql(46);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(user.stats.exp).to.be.greaterThan(previousExp);
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
});
|
||||
|
||||
// Skipped because can't stub predictableRandom correctly
|
||||
xit('gives food', () => {
|
||||
let honey = content.food.Honey;
|
||||
it('gives food', () => {
|
||||
let previousExp = user.stats.exp;
|
||||
|
||||
shared.fns.randomVal.returns(honey);
|
||||
shared.fns.predictableRandom.returns(YIELD_FOOD);
|
||||
randomVal.trueRandom.returns(YIELD_FOOD);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
|
||||
expect(user.items.food).to.eql({Honey: 1});
|
||||
expect(user.stats.exp).to.eql(0);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(user.items.food).to.not.be.empty;
|
||||
expect(user.stats.exp).to.equal(previousExp);
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
});
|
||||
|
||||
// Skipped because can't stub predictableRandom correctly
|
||||
xit('does not give equipment if all equipment has been found', () => {
|
||||
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
|
||||
user.items.gear.owned = fullArmoire;
|
||||
it('does not give equipment if all equipment has been found', () => {
|
||||
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
|
||||
user.items.gear.owned = getFullArmoire();
|
||||
user.stats.gp = 150;
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
expect(user.items.gear.owned).to.eql(fullArmoire);
|
||||
expect(user.items.gear.owned).to.eql(getFullArmoire());
|
||||
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
|
||||
|
||||
expect(armoireCount).to.eql(0);
|
||||
|
||||
expect(user.stats.exp).to.eql(30);
|
||||
expect(user.stats.gp).to.eql(50);
|
||||
expect(user.stats.gp).to.equal(50);
|
||||
});
|
||||
});
|
||||
|
||||
context('gear awards', () => {
|
||||
beforeEach(() => {
|
||||
let shield = content.gear.tree.shield.armoire.gladiatorShield;
|
||||
|
||||
shared.fns.randomVal.returns(shield);
|
||||
});
|
||||
|
||||
// Skipped because can't stub predictableRandom correctly
|
||||
xit('always drops equipment the first time', () => {
|
||||
it('always drops equipment the first time', () => {
|
||||
delete user.flags.armoireOpened;
|
||||
shared.fns.predictableRandom.returns(YIELD_EXP);
|
||||
randomVal.trueRandom.returns(YIELD_EXP);
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(1);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
shield_armoire_gladiatorShield: true,
|
||||
});
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
|
||||
|
||||
expect(armoireCount).to.eql(_.size(fullArmoire) - 1);
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 1);
|
||||
expect(user.items.food).to.be.empty;
|
||||
expect(user.stats.exp).to.eql(0);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(user.stats.exp).to.equal(0);
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
});
|
||||
|
||||
// Skipped because can't stub predictableRandom correctly
|
||||
xit('gives more equipment', () => {
|
||||
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
|
||||
it('gives more equipment', () => {
|
||||
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
|
||||
user.items.gear.owned = {
|
||||
weapon_warrior_0: true,
|
||||
head_armoire_hornedIronHelm: true,
|
||||
};
|
||||
user.stats.gp = 200;
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
buyArmoire(user);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true});
|
||||
expect(_.size(user.items.gear.owned)).to.equal(3);
|
||||
|
||||
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
|
||||
|
||||
expect(armoireCount).to.eql(_.size(fullArmoire) - 2);
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,12 +29,12 @@ describe('shared.ops.buyGear', () => {
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
|
||||
sinon.stub(shared.fns, 'randomVal');
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.fns.randomVal.restore();
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
});
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -142,6 +142,21 @@ describe('shared.ops.scoreTask', () => {
|
||||
expect(ref.beforeUser._id).to.eql(ref.afterUser._id);
|
||||
});
|
||||
|
||||
it('and increments quest progress', () => {
|
||||
expect(ref.afterUser.party.quest.progress.up).to.eql(0);
|
||||
ref.afterUser.party.quest.key = 'gryphon';
|
||||
|
||||
scoreTask({ user: ref.afterUser, task: habit, direction: 'up', cron: false });
|
||||
let firstTaskDelta = ref.afterUser.party.quest.progress.up;
|
||||
expect(firstTaskDelta).to.be.greaterThan(0);
|
||||
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(firstTaskDelta);
|
||||
|
||||
scoreTask({ user: ref.afterUser, task: habit, direction: 'up', cron: false });
|
||||
let secondTaskDelta = ref.afterUser.party.quest.progress.up - firstTaskDelta;
|
||||
expect(secondTaskDelta).to.be.greaterThan(0);
|
||||
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
it('up', () => {
|
||||
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };
|
||||
|
||||
@@ -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
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
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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true&overlay=false')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
@@ -86,6 +86,7 @@ var baseConfig = {
|
||||
if (!IS_PROD) {
|
||||
baseConfig.eslint = {
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
emitWarning: true,
|
||||
};
|
||||
}
|
||||
module.exports = baseConfig;
|
||||
@@ -23,7 +23,6 @@ module.exports = merge(baseWebpackConfig, {
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
|
||||
@@ -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: -1416px -829px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201602 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1417px -103px;
|
||||
background-position: -1157px -573px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201603 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -103px;
|
||||
background-position: -1015px -573px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201604 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -442px;
|
||||
background-position: -452px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201605 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px 0px;
|
||||
background-position: -452px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
@@ -66,73 +42,85 @@
|
||||
}
|
||||
.promo_backgrounds_armoire_201608 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -442px;
|
||||
background-position: -734px -442px;
|
||||
width: 140px;
|
||||
height: 439px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201609 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px 0px;
|
||||
background-position: -875px 0px;
|
||||
width: 139px;
|
||||
height: 438px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201610 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backtoschool {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -151px -1488px;
|
||||
background-position: -1522px -1005px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_burnout {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -151px;
|
||||
background-position: -1015px -151px;
|
||||
width: 219px;
|
||||
height: 240px;
|
||||
}
|
||||
.promo_chairs_glasses {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -220px;
|
||||
background-position: -1299px -573px;
|
||||
width: 51px;
|
||||
height: 210px;
|
||||
}
|
||||
.promo_classes_fall_2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -1013px;
|
||||
background-position: -363px -1313px;
|
||||
width: 321px;
|
||||
height: 100px;
|
||||
}
|
||||
.promo_classes_fall_2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -878px;
|
||||
background-position: -430px -1210px;
|
||||
width: 377px;
|
||||
height: 99px;
|
||||
}
|
||||
.promo_classes_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -573px;
|
||||
background-position: -1416px -480px;
|
||||
width: 103px;
|
||||
height: 348px;
|
||||
}
|
||||
.promo_contrib_spotlight_beffymaroo {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1154px -573px;
|
||||
background-position: -1696px -595px;
|
||||
width: 114px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_contrib_spotlight_cantras {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1592px -829px;
|
||||
width: 87px;
|
||||
height: 109px;
|
||||
}
|
||||
.promo_cow {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -281px -525px;
|
||||
background-position: -734px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_dilatoryDistress {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1316px -1385px;
|
||||
background-position: -1177px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_egg_mounts {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -398px;
|
||||
background-position: -1015px -868px;
|
||||
width: 280px;
|
||||
height: 147px;
|
||||
}
|
||||
@@ -144,49 +132,49 @@
|
||||
}
|
||||
.promo_enchanted_armoire_201507 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -289px -1294px;
|
||||
background-position: -1122px -1416px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201508 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -725px -1294px;
|
||||
background-position: -1340px -1416px;
|
||||
width: 180px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201509 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -666px -1488px;
|
||||
background-position: -722px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201511 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -978px;
|
||||
background-position: -1696px -1396px;
|
||||
width: 122px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201601 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -939px -1488px;
|
||||
background-position: -593px -975px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_floral_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1559px -103px;
|
||||
background-position: -1416px -1005px;
|
||||
width: 105px;
|
||||
height: 273px;
|
||||
}
|
||||
.promo_ghost_potions {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px 0px;
|
||||
background-position: -593px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_habitica {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1094px -151px;
|
||||
background-position: -1520px -480px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
@@ -196,297 +184,333 @@
|
||||
width: 305px;
|
||||
height: 304px;
|
||||
}
|
||||
.promo_habitoween_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -281px -525px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_haunted_hair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -734px;
|
||||
background-position: -1696px -1038px;
|
||||
width: 100px;
|
||||
height: 137px;
|
||||
}
|
||||
.promo_item_notif {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1385px;
|
||||
background-position: 0px -1527px;
|
||||
width: 249px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_mystery_201405 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1589px -1385px;
|
||||
background-position: -1086px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201406 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -358px -326px;
|
||||
background-position: -875px -755px;
|
||||
width: 90px;
|
||||
height: 96px;
|
||||
}
|
||||
.promo_mystery_201407 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1064px -1114px;
|
||||
background-position: -1796px -954px;
|
||||
width: 42px;
|
||||
height: 62px;
|
||||
}
|
||||
.promo_mystery_201408 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1196px -1013px;
|
||||
background-position: -1351px -655px;
|
||||
width: 60px;
|
||||
height: 71px;
|
||||
}
|
||||
.promo_mystery_201409 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1030px -1488px;
|
||||
background-position: -1268px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201410 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1131px -840px;
|
||||
background-position: -1592px -939px;
|
||||
width: 72px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201411 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1225px -1385px;
|
||||
background-position: -904px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201412 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1021px -1114px;
|
||||
background-position: -1799px -813px;
|
||||
width: 42px;
|
||||
height: 66px;
|
||||
}
|
||||
.promo_mystery_201501 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1653px -878px;
|
||||
background-position: -1796px -890px;
|
||||
width: 48px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201502 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -393px -1488px;
|
||||
background-position: -540px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201503 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -575px -1488px;
|
||||
background-position: -449px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201504 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -1030px;
|
||||
background-position: -875px -1034px;
|
||||
width: 60px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201505 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -757px -1488px;
|
||||
background-position: -452px -975px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201506 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -654px -1030px;
|
||||
background-position: -1799px -743px;
|
||||
width: 42px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201507 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1601px -1081px;
|
||||
background-position: -875px -543px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201508 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -664px -1385px;
|
||||
background-position: -875px -852px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201509 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1121px -1488px;
|
||||
background-position: -452px -884px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201510 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -758px -1385px;
|
||||
background-position: -593px -884px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201511 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1134px -1385px;
|
||||
background-position: -1359px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201512 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1638px -978px;
|
||||
background-position: -1351px -573px;
|
||||
width: 60px;
|
||||
height: 81px;
|
||||
}
|
||||
.promo_mystery_201601 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -975px;
|
||||
background-position: -1286px -392px;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201602 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1407px -1385px;
|
||||
background-position: -995px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201603 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1498px -1385px;
|
||||
background-position: -1521px -1416px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201604 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -852px -1385px;
|
||||
background-position: -875px -943px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201605 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -302px -1488px;
|
||||
background-position: -813px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201606 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -358px -220px;
|
||||
background-position: -875px -649px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201607 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -484px -1488px;
|
||||
background-position: -631px -1527px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201608 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1040px -1385px;
|
||||
background-position: -734px -973px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201609 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -946px -1385px;
|
||||
background-position: -734px -882px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201610 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1350px -302px;
|
||||
width: 63px;
|
||||
height: 84px;
|
||||
}
|
||||
.promo_mystery_3014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -507px -1294px;
|
||||
background-position: -904px -1416px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_orca {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -872px;
|
||||
background-position: -1696px -1290px;
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_partyhats {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1094px -327px;
|
||||
background-position: -1696px -1563px;
|
||||
width: 115px;
|
||||
height: 47px;
|
||||
}
|
||||
.promo_pastel_skin {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -331px -1210px;
|
||||
background-position: -808px -1210px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.customize-option.promo_pastel_skin {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -356px -1225px;
|
||||
background-position: -833px -1225px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_peppermint_flame {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -593px -882px;
|
||||
background-position: -1696px -151px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pet_skins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1556px -398px;
|
||||
background-position: -1696px -299px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_pet_skins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1581px -413px;
|
||||
background-position: -1721px -314px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_pyromancer {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1696px -1176px;
|
||||
width: 113px;
|
||||
height: 113px;
|
||||
}
|
||||
.promo_rainbow_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -875px -439px;
|
||||
width: 92px;
|
||||
height: 103px;
|
||||
}
|
||||
.promo_seasonal_shop_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1416px 0px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_shimmer_hair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1210px;
|
||||
background-position: -685px -1313px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.promo_splashyskins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -250px -1385px;
|
||||
background-position: -250px -1527px;
|
||||
width: 198px;
|
||||
height: 91px;
|
||||
}
|
||||
.customize-option.promo_splashyskins {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -275px -1400px;
|
||||
background-position: -275px -1542px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_spooky_sparkles_fall_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -220px;
|
||||
width: 140px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_spring_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -978px;
|
||||
background-position: 0px -1313px;
|
||||
width: 362px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_springclasses2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1294px;
|
||||
background-position: -326px -1416px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_springclasses2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -978px -749px;
|
||||
background-position: -615px -1416px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_staff_spotlight_Lemoness {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -439px;
|
||||
background-position: -1696px -743px;
|
||||
width: 102px;
|
||||
height: 146px;
|
||||
}
|
||||
.promo_staff_spotlight_Viirus {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1145px -392px;
|
||||
background-position: -1696px -447px;
|
||||
width: 119px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_staff_spotlight_paglias {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -734px -586px;
|
||||
background-position: -1696px -890px;
|
||||
width: 99px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_summer_classes_2014 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px 0px;
|
||||
background-position: 0px -1210px;
|
||||
width: 429px;
|
||||
height: 102px;
|
||||
}
|
||||
@@ -498,55 +522,61 @@
|
||||
}
|
||||
.promo_summer_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px 0px;
|
||||
background-position: -1015px 0px;
|
||||
width: 400px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_takeThis_gear {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -431px;
|
||||
background-position: -1235px -302px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_takethis_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -906px -1294px;
|
||||
background-position: -1286px -483px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1204px -840px;
|
||||
background-position: -936px -1034px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_unconventional_armor2 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1299px -784px;
|
||||
width: 70px;
|
||||
height: 74px;
|
||||
}
|
||||
.promo_updos {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1522px -546px;
|
||||
background-position: -1520px -656px;
|
||||
width: 156px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_veteran_pets {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -1114px;
|
||||
background-position: -1696px -1487px;
|
||||
width: 146px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_winter_classes_2016 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -922px;
|
||||
background-position: -1015px -1016px;
|
||||
width: 360px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_winterclasses2015 {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -1081px;
|
||||
background-position: 0px -1416px;
|
||||
width: 325px;
|
||||
height: 110px;
|
||||
}
|
||||
.promo_winteryhair {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -978px -840px;
|
||||
background-position: -1522px -1156px;
|
||||
width: 152px;
|
||||
height: 75px;
|
||||
}
|
||||
@@ -558,7 +588,7 @@
|
||||
}
|
||||
.npc_viirus {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -449px -1385px;
|
||||
background-position: -1296px -868px;
|
||||
width: 108px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -570,31 +600,31 @@
|
||||
}
|
||||
.scene_coding {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1488px;
|
||||
background-position: -1696px 0px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.scene_phone_peek {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1522px -712px;
|
||||
background-position: -1235px -151px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.welcome_basic_avatars {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -546px;
|
||||
background-position: -1416px -148px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
.welcome_promo_party {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -874px -392px;
|
||||
background-position: -1015px -392px;
|
||||
width: 270px;
|
||||
height: 180px;
|
||||
}
|
||||
.welcome_sample_tasks {
|
||||
background-image: url(/spritesmith-largeSprites-0.png);
|
||||
background-position: -1275px -712px;
|
||||
background-position: -1416px -314px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 692 KiB After Width: | Height: | Size: 776 KiB |
962
website/assets/sprites/dist/spritesmith-main-0.css
vendored
962
website/assets/sprites/dist/spritesmith-main-0.css
vendored
File diff suppressed because it is too large
Load Diff
BIN
website/assets/sprites/dist/spritesmith-main-0.png
vendored
BIN
website/assets/sprites/dist/spritesmith-main-0.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 438 KiB |
1948
website/assets/sprites/dist/spritesmith-main-1.css
vendored
1948
website/assets/sprites/dist/spritesmith-main-1.css
vendored
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user