Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93437a00dd | |||
| 31afa0345e | |||
| b2c278b00a | |||
| b54037f230 | |||
| 488dcb916a | |||
| 10792b65be | |||
| 390970a73a | |||
| 6f0d0b1fb3 | |||
| dfcd32d54a | |||
| c24cbdc987 | |||
| 68d8c0de51 | |||
| a7b0fa195f | |||
| ab65aa692f | |||
| 198d4df02a | |||
| 6fa2f643fd | |||
| 4d39861b51 | |||
| d4c99b6db6 | |||
| a79dc90a45 | |||
| daa6bd0315 | |||
| 99f2373214 | |||
| 374d528647 | |||
| 8550ca4d29 | |||
| 79f6b59f6e | |||
| e37ad420ce | |||
| 2f9ff92cbd | |||
| 56479e7fbd | |||
| 66e4849553 | |||
| 9e29b44ad9 | |||
| 605488dd47 | |||
| 6c16b4b77e | |||
| 80205dcc67 | |||
| 65d5bf69f6 | |||
| 20792f5455 | |||
| 6fd509df13 | |||
| d7df18a97a | |||
| ff5c44c5a8 | |||
| c929fa1351 | |||
| d30e7b9251 | |||
| c6c6632405 | |||
| e0d9ecca52 | |||
| 78f187f87a | |||
| af047d3c82 | |||
| 20e8f47def | |||
| c496f94c79 | |||
| ad6073220d | |||
| d669db0f9a | |||
| c244fe488d | |||
| aef71db0f5 | |||
| ac566882fe | |||
| 1ad662a090 | |||
| 1db52de45d | |||
| f8cfdfa37d | |||
| ffd36465c9 | |||
| dbb1e3aa18 | |||
| 07ce68c596 | |||
| 4c5d72c96f | |||
| 7121653515 | |||
| ce2b66a2be | |||
| 43e381cfc1 | |||
| ca5161b48a | |||
| 44dec6dc4b | |||
| fb8f0bed43 | |||
| c391de9e86 | |||
| 7a534ab81d | |||
| 32c7f40c4b | |||
| 569fbff244 | |||
| 3fe847f329 | |||
| d52d759733 | |||
| 5e5a755022 | |||
| 8d148b4d69 | |||
| 248b64a43f | |||
| 57ed0f0a10 | |||
| 3455adaef5 | |||
| 4d0295a60d | |||
| a002bc5e20 | |||
| b9d1086e24 | |||
| 412a0ecc8c | |||
| d44c9ea853 | |||
| c44c581265 | |||
| f666f3cd04 | |||
| 236bd6cec4 | |||
| e124c36274 | |||
| d5511a0047 | |||
| 87f003f392 | |||
| 930a869365 | |||
| 809da8add0 | |||
| 1dad176320 | |||
| 1596c6218f | |||
| 0d3aba950a | |||
| f1110e0f89 | |||
| 7273f8f6d9 | |||
| a0ae200a54 | |||
| ca448f081d | |||
| cd27afa9f0 | |||
| 8bc8183895 | |||
| c26c52f1fe | |||
| 8d5becc9ce | |||
| cf7f6e2a67 | |||
| acad3b8873 | |||
| 04f4eb8490 | |||
| 8c8af83dfc | |||
| 86d65956d9 | |||
| 42c5e6c22b | |||
| 79b51a40ce |
@@ -1,7 +1,15 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- $CXX --version
|
||||
- npm install -g npm@4
|
||||
- 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:
|
||||
@@ -12,6 +20,9 @@ after_script:
|
||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
||||
script: npm run $TEST
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- DISABLE_REQUEST_LOGGING=true
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
"PORT":3000,
|
||||
"ENABLE_CONSOLE_LOGS_IN_PROD":"false",
|
||||
"IP":"0.0.0.0",
|
||||
"CORES":1,
|
||||
"WEB_CONCURRENCY":1,
|
||||
"BASE_URL":"http://localhost:3000",
|
||||
"FACEBOOK_ANALYTICS":"1234567890123456",
|
||||
"FACEBOOK_KEY":"123456789012345",
|
||||
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"GOOGLE_CLIENT_ID":"123456789012345",
|
||||
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"PLAY_API": {
|
||||
"CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"ACCESS_TOKEN":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"REFRESH_TOKEN":"aaaabbbbccccddddeeeeffff00001111"
|
||||
},
|
||||
"NODE_DB_URI":"mongodb://localhost/habitrpg",
|
||||
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
|
||||
"NODE_ENV":"development",
|
||||
@@ -81,5 +86,6 @@
|
||||
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
|
||||
}
|
||||
},
|
||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
var migrationName = '20170131_habit_birthday.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 2017 party robes if user has 2016 robes, 2016 robes if they have the 2015 robes,
|
||||
* 2015 robes if they have the 2014 robes, and 2014 robes otherwise. Also cake!
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2017-01-24')}, // remove after first run to cover remaining users
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data)
|
||||
'items.gear.owned'
|
||||
],
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {'migration':migrationName};
|
||||
if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2016')) {
|
||||
set['items.gear.owned.armor_special_birthday2017'] = false;
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2015')) {
|
||||
set['items.gear.owned.armor_special_birthday2016'] = false;
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday')) {
|
||||
set['items.gear.owned.armor_special_birthday2015'] = false;
|
||||
} else {
|
||||
set['items.gear.owned.armor_special_birthday'] = false;
|
||||
}
|
||||
|
||||
var inc = {
|
||||
'items.food.Cake_Skeleton':1,
|
||||
'items.food.Cake_Base':1,
|
||||
'items.food.Cake_CottonCandyBlue':1,
|
||||
'items.food.Cake_CottonCandyPink':1,
|
||||
'items.food.Cake_Shade':1,
|
||||
'items.food.Cake_White':1,
|
||||
'items.food.Cake_Golden':1,
|
||||
'items.food.Cake_Zombie':1,
|
||||
'items.food.Cake_Desert':1,
|
||||
'items.food.Cake_Red':1,
|
||||
'achievements.habitBirthdays':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);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['head_mystery_201612','armor_mystery_201612']
|
||||
$each:['head_mystery_201702','back_mystery_201702']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
@@ -45,10 +44,10 @@ function updateUsers (users) {
|
||||
return;
|
||||
}
|
||||
|
||||
var userPaymentPromises = users.map(updateUser);
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPaymentPromises)
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
|
||||
@@ -6,49 +6,69 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
* Remove flag stating that the Enchanted Armoire is empty, for when new equipment is added
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.armoireEmpty': true,
|
||||
};
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.armoireEmpty':true
|
||||
};
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
};
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
|
||||
var set = {'migration': migrationName, 'flags.armoireEmpty': false};
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
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!'; }
|
||||
@@ -58,3 +78,5 @@ function exiting(code, msg) {
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var migrationName = '20170103_takeThis.js'; // Update per month
|
||||
var migrationName = '20170201_takeThis.js'; // Update per month
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
@@ -6,41 +6,64 @@ 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 monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['b1d436b5-c784-42e3-9b07-7072479a6f8e']} // Update per month
|
||||
};
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['ff674aba-a114-4a6f-8ebc-1de27ffb646e']}
|
||||
};
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.gear.owned',
|
||||
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName};
|
||||
{ else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
@@ -54,19 +77,17 @@ dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
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!'; }
|
||||
@@ -77,4 +98,4 @@ function exiting(code, msg) {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.71.0",
|
||||
"version": "3.78.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.2",
|
||||
"amazon-payments": "0.0.4",
|
||||
"amplitude": "^2.0.3",
|
||||
"apidoc": "^0.16.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^1.7.6",
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
@@ -25,6 +25,7 @@
|
||||
"babel-register": "^6.6.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"babelify": "^7.2.0",
|
||||
"bcrypt": "^1.0.2",
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bower": "~1.3.12",
|
||||
@@ -33,7 +34,7 @@
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.23.1",
|
||||
"css-loader": "^0.26.1",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
@@ -41,8 +42,8 @@
|
||||
"express": "~4.14.0",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.8.4",
|
||||
"extract-text-webpack-plugin": "^2.0.0-rc.3",
|
||||
"file-loader": "^0.10.0",
|
||||
"glob": "^4.3.5",
|
||||
"got": "^6.1.1",
|
||||
"grunt": "~0.4.1",
|
||||
@@ -67,9 +68,8 @@
|
||||
"image-size": "~0.3.2",
|
||||
"in-app-purchase": "^1.1.6",
|
||||
"jade": "~1.11.0",
|
||||
"jquery": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz",
|
||||
"jquery": "^3.1.1",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"lodash": "^3.10.1",
|
||||
@@ -82,22 +82,21 @@
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
"newrelic": "^1.27.2",
|
||||
"nib": "^1.1.0",
|
||||
"node-gcm": "^0.14.4",
|
||||
"nodemailer": "^2.3.2",
|
||||
"object-path": "^0.9.2",
|
||||
"ora": "^0.2.0",
|
||||
"ora": "^1.1.0",
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "~0.2.1",
|
||||
"passport-facebook": "2.0.0",
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.2.1",
|
||||
"postcss-easy-import": "^1.0.1",
|
||||
"postcss-easy-import": "^2.0.0",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta6",
|
||||
"pug": "^2.0.0-beta11",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.74.0",
|
||||
@@ -106,23 +105,23 @@
|
||||
"s3-upload-stream": "^1.0.6",
|
||||
"semantic-ui-less": "~2.2.4",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"shelljs": "^0.6.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"stripe": "^4.2.0",
|
||||
"superagent": "^1.8.3",
|
||||
"superagent": "^3.4.3",
|
||||
"universal-analytics": "~0.3.2",
|
||||
"url-loader": "^0.5.7",
|
||||
"useragent": "2.1.9",
|
||||
"uuid": "^2.0.1",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.1.0",
|
||||
"vue-hot-reload-api": "^1.2.0",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-template-compiler": "^2.1.0",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-merge": "^0.8.3",
|
||||
"vue-style-loader": "^2.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^2.6.1",
|
||||
"winston": "^2.1.0",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
@@ -152,36 +151,37 @@
|
||||
"sprites": "gulp sprites:compile",
|
||||
"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:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "cross-env NODE_ENV=test 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",
|
||||
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
"chai": "^3.4.0",
|
||||
"chai-as-promised": "^5.1.0",
|
||||
"chalk": "^1.1.3",
|
||||
"chromedriver": "^2.21.2",
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"cross-spawn": "^2.1.5",
|
||||
"cross-env": "^3.1.4",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
"eslint": "^3.0.0",
|
||||
"eslint-config-habitrpg": "^2.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.3.0",
|
||||
"eslint-plugin-html": "^1.3.0",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-mocha": "^4.7.0",
|
||||
"event-stream": "^3.2.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"expect.js": "~0.2.0",
|
||||
"grunt-karma": "~0.12.1",
|
||||
"http-proxy-middleware": "^0.12.0",
|
||||
"inject-loader": "^2.0.1",
|
||||
"isparta-loader": "^2.0.0",
|
||||
"http-proxy-middleware": "^0.17.0",
|
||||
"inject-loader": "^3.0.0-beta4",
|
||||
"istanbul": "^0.3.14",
|
||||
"karma": "^1.3.0",
|
||||
"karma-babel-preprocessor": "^6.0.1",
|
||||
@@ -193,25 +193,26 @@
|
||||
"karma-sinon-chai": "^1.2.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.24",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"karma-webpack": "^2.0.2",
|
||||
"lcov-result-merger": "^1.0.2",
|
||||
"lolex": "^1.4.0",
|
||||
"mocha": "^2.3.3",
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~2.1.0",
|
||||
"monk": "^3.1.3",
|
||||
"nightwatch": "^0.8.18",
|
||||
"monk": "^4.0.0",
|
||||
"nightwatch": "^0.9.12",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
"require-again": "^2.0.0",
|
||||
"rewire": "^2.3.3",
|
||||
"selenium-server": "2.53.0",
|
||||
"selenium-server": "^3.0.1",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-dev-middleware": "^1.4.0",
|
||||
"webpack-hot-middleware": "^2.6.0"
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,14 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updates contributor level, balance, ads, blocked', async () => {
|
||||
it('change contributor level, balance, ads', async () => {
|
||||
let hero = await generateUser();
|
||||
let prevBlockState = hero.auth.blocked;
|
||||
let prevSleepState = hero.preferences.sleep;
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
balance: 3,
|
||||
contributor: {level: 1},
|
||||
purchased: {ads: true},
|
||||
auth: {blocked: true},
|
||||
});
|
||||
|
||||
// test response
|
||||
@@ -61,18 +62,47 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(heroRes.contributor.level).to.equal(1);
|
||||
expect(heroRes.purchased.ads).to.equal(true);
|
||||
expect(heroRes.auth.blocked).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(hero.contributor.level).to.equal(1);
|
||||
expect(hero.purchased.ads).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(true);
|
||||
expect(hero.preferences.sleep).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||
expect(hero.preferences.sleep).to.equal(prevSleepState);
|
||||
expect(hero.notifications.length).to.equal(1);
|
||||
expect(hero.notifications[0].type).to.equal('NEW_CONTRIBUTOR_LEVEL');
|
||||
});
|
||||
|
||||
it('block a user', async () => {
|
||||
let hero = await generateUser();
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
auth: {blocked: true},
|
||||
preferences: {sleep: true},
|
||||
});
|
||||
|
||||
// test response values
|
||||
expect(heroRes.auth.blocked).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.auth.blocked).to.equal(true);
|
||||
expect(hero.preferences.sleep).to.equal(true);
|
||||
});
|
||||
|
||||
it('unblock a user', async () => {
|
||||
let hero = await generateUser();
|
||||
let prevSleepState = hero.preferences.sleep;
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
auth: {blocked: false},
|
||||
});
|
||||
|
||||
// test response values
|
||||
expect(heroRes.auth.blocked).to.equal(false);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.auth.blocked).to.equal(false);
|
||||
expect(hero.preferences.sleep).to.equal(prevSleepState);
|
||||
});
|
||||
|
||||
it('updates chatRevoked flag', async () => {
|
||||
let hero = await generateUser();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import amzLib from '../../../../../../website/server/libs/amazonPayments';
|
||||
|
||||
describe('payments : amazon #subscribeCancel', () => {
|
||||
let endpoint = '/amazon/subscribe/cancel';
|
||||
let endpoint = '/amazon/subscribe/cancel?noRedirect=true';
|
||||
let user, group, amazonSubscribeCancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -22,7 +22,7 @@ describe('payments : amazon #subscribeCancel', () => {
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('payments : amazon #subscribeCancel', () => {
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?groupId=${group._id}`);
|
||||
await user.get(`${endpoint}&groupId=${group._id}`);
|
||||
|
||||
expect(amazonSubscribeCancelStub).to.be.calledOnce;
|
||||
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('payments - amazon - #subscribe', () => {
|
||||
let subscription = 'basic_3mo';
|
||||
let coupon;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #cancelSubscribe', () => {
|
||||
let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let cancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
cancelStub = sinon.stub(applePayments, 'cancelSubscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.cancelSubscribe.restore();
|
||||
});
|
||||
|
||||
it('cancels the subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.paymentMethod': 'Apple',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
|
||||
expect(cancelStub).to.be.calledOnce;
|
||||
expect(cancelStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(cancelStub.args[0][1]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(cancelStub.args[0][1]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #verify', () => {
|
||||
let endpoint = '/iap/ios/verify';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.verifyGemPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint, {
|
||||
transaction: {
|
||||
receipt: 'receipt',
|
||||
}});
|
||||
|
||||
expect(verifyStub).to.be.calledOnce;
|
||||
expect(verifyStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(verifyStub.args[0][1]).to.eql('receipt');
|
||||
expect(verifyStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(verifyStub.args[0][2]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #subscribe', () => {
|
||||
let endpoint = '/iap/ios/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies sub key', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let subscribeStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
subscribeStub = sinon.stub(applePayments, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.subscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
let sku = 'com.habitrpg.ios.habitica.subscription.3month';
|
||||
|
||||
await user.post(endpoint, {
|
||||
sku,
|
||||
receipt: 'receipt',
|
||||
});
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #cancelSubscribe', () => {
|
||||
let endpoint = '/iap/android/subscribe/cancel?noRedirect=true';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let cancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
cancelStub = sinon.stub(googlePayments, 'cancelSubscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.cancelSubscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.paymentMethod': 'Google',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
|
||||
expect(cancelStub).to.be.calledOnce;
|
||||
expect(cancelStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(cancelStub.args[0][1]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(cancelStub.args[0][1]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #subscribe', () => {
|
||||
let endpoint = '/iap/android/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies sub key', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let subscribeStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
subscribeStub = sinon.stub(googlePayments, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.subscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
let sku = 'com.habitrpg.android.habitica.subscription.3month';
|
||||
|
||||
await user.post(endpoint, {
|
||||
sku,
|
||||
transaction: {receipt: 'receipt', signature: 'signature'},
|
||||
});
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][3]).to.eql('signature');
|
||||
expect(subscribeStub.args[0][4]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][4]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #verify', () => {
|
||||
let endpoint = '/iap/android/verify';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.verifyGemPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint, {
|
||||
transaction: {receipt: 'receipt', signature: 'signature'},
|
||||
});
|
||||
|
||||
expect(verifyStub).to.be.calledOnce;
|
||||
expect(verifyStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(verifyStub.args[0][1]).to.eql('receipt');
|
||||
expect(verifyStub.args[0][2]).to.eql('signature');
|
||||
expect(verifyStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(verifyStub.args[0][3]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('payments : paypal #checkout', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
await user.get(`${endpoint}?noRedirect=true`);
|
||||
|
||||
expect(checkoutStub).to.be.calledOnce;
|
||||
expect(checkoutStub.args[0][0].gift).to.eql(undefined);
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('payments : paypal #checkoutSuccess', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}`);
|
||||
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}&noRedirect=true`);
|
||||
|
||||
expect(checkoutSuccessStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('payments : paypal #subscribe', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?sub=${subKey}`);
|
||||
await user.get(`${endpoint}?sub=${subKey}&noRedirect=true`);
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('payments : paypal #subscribeCancel', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
await user.get(`${endpoint}?noRedirect=true`);
|
||||
|
||||
expect(subscribeCancelStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('payments : paypal #subscribeSuccess', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?token=${token}`);
|
||||
await user.get(`${endpoint}?token=${token}&noRedirect=true`);
|
||||
|
||||
expect(subscribeSuccessStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import stripePayments from '../../../../../../website/server/libs/stripePayments';
|
||||
|
||||
describe('payments - stripe - #subscribeCancel', () => {
|
||||
let endpoint = '/stripe/subscribe/cancel';
|
||||
let endpoint = '/stripe/subscribe/cancel?redirect=none';
|
||||
let user, group, stripeCancelSubscriptionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -39,7 +39,7 @@ describe('payments - stripe - #subscribeCancel', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?redirect=none`);
|
||||
await user.get(`${endpoint}`);
|
||||
|
||||
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
@@ -64,7 +64,7 @@ describe('payments - stripe - #subscribeCancel', () => {
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?groupId=${group._id}&redirect=none`);
|
||||
await user.get(`${endpoint}&groupId=${group._id}`);
|
||||
|
||||
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
let completedTodos = await user.get('/tasks/user?type=completedTodos');
|
||||
let todos = await user.get('/tasks/user?type=todos');
|
||||
let allTodos = todos.concat(completedTodos);
|
||||
expect(allTodos.length).to.equal(initialTodoCount + 4); // + 6 - 3 completed (but one is from challenge)
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 7');
|
||||
expect(allTodos.length).to.equal(initialTodoCount + 5); // + 7 - 3 completed (but one is from challenge)
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 6'); // last completed todo
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
map,
|
||||
} from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
|
||||
describe('DELETE /user', () => {
|
||||
let user;
|
||||
@@ -67,6 +71,30 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// delete the user
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
let party;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('GET /user', () => {
|
||||
let returnedUser = await user.get('/user');
|
||||
|
||||
expect(returnedUser.auth.local.hashed_password).to.not.exist;
|
||||
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
@@ -186,6 +187,40 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(group.chat[0].uuid).to.equal('system');
|
||||
});
|
||||
|
||||
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test challenge habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
|
||||
await user.post('/user/class/cast/brightness');
|
||||
await user.sync();
|
||||
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
|
||||
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
});
|
||||
|
||||
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.challenge.id === challenge._id;
|
||||
});
|
||||
|
||||
expect(userChallengeTask).to.exist;
|
||||
expect(syncedGroupTask).to.exist;
|
||||
expect(userChallengeTask.value).to.equal(0);
|
||||
expect(syncedGroupTask.value).to.equal(0);
|
||||
});
|
||||
|
||||
// TODO find a way to have sinon working in integration tests
|
||||
// it doesn't work when tests are running separately from server
|
||||
it('passes correct target to spell when targetType === \'task\'');
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
encrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import superagent from 'superagent';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const API_TEST_SERVER_PORT = nconf.get('PORT');
|
||||
|
||||
describe('GET /user/auth/local/reset-password-set-new-one', () => {
|
||||
let endpoint = `http://localhost:${API_TEST_SERVER_PORT}/static/user/auth/local/reset-password-set-new-one`;
|
||||
|
||||
// Tests to validate the validatePasswordResetCodeAndFindUser function
|
||||
|
||||
it('renders an error page if the code is missing', async () => {
|
||||
try {
|
||||
await superagent.get(endpoint);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is invalid json', async () => {
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=invalid`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
try {
|
||||
let code = JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
});
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user does not exist', async () => {
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user has no local auth', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
it('returns the password reset page if the password reset code is valid', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent.get(`${endpoint}?code=${code}`);
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import {
|
||||
encrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
import {
|
||||
compare,
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import superagent from 'superagent';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const API_TEST_SERVER_PORT = nconf.get('PORT');
|
||||
|
||||
describe('POST /user/auth/local/reset-password-set-new-one', () => {
|
||||
let endpoint = `http://localhost:${API_TEST_SERVER_PORT}/static/user/auth/local/reset-password-set-new-one`;
|
||||
|
||||
// Tests to validate the validatePasswordResetCodeAndFindUser function
|
||||
|
||||
it('renders an error page if the code is missing', async () => {
|
||||
try {
|
||||
await superagent.post(endpoint);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is invalid json', async () => {
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=invalid`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
try {
|
||||
let code = JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
});
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user does not exist', async () => {
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user has no local auth', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
it('renders the error page if the new password is missing', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the error page if the password confirmation is missing', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({newPassword: 'my new password'});
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the error page if the password confirmation does not match', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'not matching',
|
||||
});
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the success page and save the user', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
});
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.equal(undefined);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
let isPassValid = await compare(user, 'my new password');
|
||||
expect(isPassValid).to.equal(true);
|
||||
});
|
||||
|
||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
});
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.equal(undefined);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('my new password', user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,11 @@ import {
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
describe('POST /user/auth/local/login', () => {
|
||||
let api;
|
||||
@@ -72,4 +77,35 @@ describe('POST /user/auth/local/login', () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// login
|
||||
await api.post(endpoint, {
|
||||
username: user.auth.local.email,
|
||||
password: textPassword,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
decrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
|
||||
describe('POST /user/reset-password', async () => {
|
||||
let endpoint = '/user/reset-password';
|
||||
@@ -35,4 +39,19 @@ describe('POST /user/reset-password', async () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sets a new password reset code on user.auth.local that expires in 1 day', async () => {
|
||||
expect(user.auth.local.passwordResetCode).to.be.undefined;
|
||||
|
||||
await user.post(endpoint, {
|
||||
email: user.auth.local.email,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.passwordResetCode).to.be.a.string;
|
||||
let decryptedCode = JSON.parse(decrypt(user.auth.local.passwordResetCode));
|
||||
expect(decryptedCode.userId).to.equal(user._id);
|
||||
expect(moment(decryptedCode.expiresAt).isAfter(moment().add({hours: 23}))).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,11 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-email';
|
||||
|
||||
@@ -66,6 +71,41 @@ describe('PUT /user/auth/update-email', () => {
|
||||
message: t('cannotFulfillReq'),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
let myNewEmail = 'my-new-random-email@example.net';
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
let response = await user.put(ENDPOINT, {
|
||||
newEmail: myNewEmail,
|
||||
password: textPassword,
|
||||
});
|
||||
expect(response).to.eql({ email: myNewEmail });
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.email).to.equal(myNewEmail);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('Social Login User', async () => {
|
||||
|
||||
@@ -2,6 +2,11 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-password';
|
||||
|
||||
@@ -89,4 +94,36 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
await user.put(ENDPOINT, {
|
||||
password: textPassword,
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(newPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,11 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-username';
|
||||
|
||||
@@ -24,6 +29,41 @@ describe('PUT /user/auth/update-username', async () => {
|
||||
expect(user.auth.local.username).to.eql(newUsername);
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let myNewUsername = 'my-new-username';
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: myNewUsername,
|
||||
password: textPassword,
|
||||
});
|
||||
expect(response).to.eql({ username: myNewUsername });
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.username).to.eql(myNewUsername);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
context('errors', async () => {
|
||||
it('prevents username update if new username is already taken', async () => {
|
||||
let existingUsername = 'existing-username';
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import applePayments from '../../../../../website/server/libs/applePayments';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Apple Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
sku = 'com.habitrpg.ios.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await applePayments.verifyGemPurchase(user, receipt, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.ios.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: 'wrongsku',
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -975,19 +975,18 @@ describe('recoverCron', () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async (done) => {
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Bluebird.resolve(null));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async (done) => {
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
execStub.onCall(4).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'}));
|
||||
|
||||
@@ -995,20 +994,17 @@ describe('recoverCron', () => {
|
||||
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({_cronSignature: 'NOT_RUNNING'});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async (done) => {
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import googlePayments from '../../../../../website/server/libs/googlePayments';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, signature, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if productId is invalid', async () => {
|
||||
receipt = `{"token": "${token}", "productId": "invalid"}`;
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, signature, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.android.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if sku is invalid', async () => {
|
||||
sku = 'invalid';
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: {data: receipt, signature},
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, signature, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.paymentMethod = googlePayments.constants.PAYMENT_METHOD_GOOGLE;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = {data: receipt, signature};
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,337 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
encrypt as encryptPassword,
|
||||
makeSalt,
|
||||
encrypt,
|
||||
} from '../../../../../website/server/libs/encryption';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
sha1MakeSalt,
|
||||
bcryptHash,
|
||||
bcryptCompare,
|
||||
compare,
|
||||
convertToBcrypt,
|
||||
validatePasswordResetCodeAndFindUser,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
|
||||
describe('Password Utilities', () => {
|
||||
describe('Encrypt', () => {
|
||||
it('always encrypt the same password to the same value when using the same salt', () => {
|
||||
describe('compare', () => {
|
||||
it('can compare a correct password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = makeSalt();
|
||||
let encryptedPassword = encryptPassword(textPassword, salt);
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(encryptPassword(textPassword, salt)).to.eql(encryptedPassword);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('never encrypt the same password to the same value when using a different salt', () => {
|
||||
it('can compare an invalid password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let aSalt = makeSalt();
|
||||
let anotherSalt = makeSalt();
|
||||
let anEncryptedPassword = encryptPassword(textPassword, aSalt);
|
||||
let anotherEncryptedPassword = encryptPassword(textPassword, anotherSalt);
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('can compare a correct password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can compare an invalid password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await compare(null, 'some password');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if passwordToCheck is missing', async () => {
|
||||
try {
|
||||
await compare({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if an invalid hashing method is used', async () => {
|
||||
try {
|
||||
await compare({
|
||||
auth: {
|
||||
local: {
|
||||
passwordHashMethod: 'invalid',
|
||||
},
|
||||
},
|
||||
}, 'pass');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: Invalid password hash method.');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Make Salt', () => {
|
||||
it('creates a salt with length 10 by default', () => {
|
||||
let salt = makeSalt();
|
||||
describe('convertToBcrypt', () => {
|
||||
it('converts an user password hashed with sha1 to bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(salt.length).to.eql(10);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await convertToBcrypt(user, textPassword);
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.hashed_password).to.be.a.string;
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can create a salt of any length', () => {
|
||||
let length = 24;
|
||||
let salt = makeSalt(length);
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt(null, 'string');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
expect(salt.length).to.eql(length);
|
||||
it('throws an error if plainTextPassword is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePasswordResetCodeAndFindUser', () => {
|
||||
it('returns false if the code is missing', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser();
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is invalid json', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser('invalid json');
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
let res = await validatePasswordResetCodeAndFindUser(JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
}));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user does not exist', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user has no local auth', async () => {
|
||||
let user = await generateUser({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns the user if the password reset code is valid', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).not.to.equal(false);
|
||||
expect(res._id).to.equal(user._id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bcrypt', () => {
|
||||
describe('Hash', () => {
|
||||
it('returns a hashed string', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
expect(hashedPassword).to.be.a.string;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Compare', () => {
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHA1', () => {
|
||||
describe('Encrypt', () => {
|
||||
it('always encrypt the same password to the same value when using the same salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let encryptedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(sha1EncryptPassword(textPassword, salt)).to.eql(encryptedPassword);
|
||||
});
|
||||
|
||||
it('never encrypt the same password to the same value when using a different salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let aSalt = sha1MakeSalt();
|
||||
let anotherSalt = sha1MakeSalt();
|
||||
let anEncryptedPassword = sha1EncryptPassword(textPassword, aSalt);
|
||||
let anotherEncryptedPassword = sha1EncryptPassword(textPassword, anotherSalt);
|
||||
|
||||
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Make Salt', () => {
|
||||
it('creates a salt with length 10 by default', () => {
|
||||
let salt = sha1MakeSalt();
|
||||
|
||||
expect(salt.length).to.eql(10);
|
||||
});
|
||||
|
||||
it('can create a salt of any length', () => {
|
||||
let length = 24;
|
||||
let salt = sha1MakeSalt(length);
|
||||
|
||||
expect(salt.length).to.eql(length);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,25 +91,23 @@ describe('Task Model', () => {
|
||||
sandbox.spy(Tasks.Task, 'findOne');
|
||||
});
|
||||
|
||||
it('throws an error if task identifier is not passed in', async (done) => {
|
||||
it('throws an error if task identifier is not passed in', async () => {
|
||||
try {
|
||||
await Tasks.Task.findByIdOrAlias(null, user._id);
|
||||
throw new Error('No exception when Id is None');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.eql(new InternalServerError('Task identifier is a required argument'));
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if user identifier is not passed in', async (done) => {
|
||||
it('throws an error if user identifier is not passed in', async () => {
|
||||
try {
|
||||
await Tasks.Task.findByIdOrAlias(taskWithAlias._id);
|
||||
throw new Error('No exception when user_id is undefined');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err).to.eql(new InternalServerError('User identifier is a required argument'));
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
describe('Settings Controller', function () {
|
||||
var rootScope, scope, user, User, ctrl;
|
||||
var rootScope, scope, $httpBackend, user, User, ctrl, Notification;
|
||||
|
||||
const actionClickEvent = {
|
||||
target: document.createElement('button'),
|
||||
@@ -29,18 +29,27 @@ describe('Settings Controller', function () {
|
||||
releaseBoth: sandbox.stub(),
|
||||
};
|
||||
|
||||
Notification = {
|
||||
error: sandbox.stub(),
|
||||
text: sandbox.stub()
|
||||
};
|
||||
|
||||
$provide.value('Notification', Notification);
|
||||
$provide.value('User', User);
|
||||
$provide.value('Guide', sandbox.stub());
|
||||
});
|
||||
|
||||
inject(function(_$rootScope_, _$controller_) {
|
||||
inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
|
||||
scope = _$rootScope_.$new();
|
||||
rootScope = _$rootScope_;
|
||||
$httpBackend = _$httpBackend_;
|
||||
|
||||
$httpBackend.whenGET(/partials/).respond();
|
||||
|
||||
// Load RootCtrl to ensure shared behaviors are loaded
|
||||
_$controller_('RootCtrl', {$scope: scope, User: User});
|
||||
_$controller_('RootCtrl', {$scope: scope, User: User, Notification: Notification});
|
||||
|
||||
ctrl = _$controller_('SettingsCtrl', {$scope: scope, User: User});
|
||||
ctrl = _$controller_('SettingsCtrl', {$scope: scope, User: User, Notification: Notification});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,4 +290,48 @@ describe('Settings Controller', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Validating coupons', function () {
|
||||
describe('#applyCoupon', function () {
|
||||
it('displays an error when an invalid coupon is applied', function () {
|
||||
$httpBackend
|
||||
.whenPOST('/api/v3/coupons/validate/INVALID_COUPON?userV=undefined')
|
||||
.respond(200, {
|
||||
success: true,
|
||||
data: {
|
||||
valid: false
|
||||
},
|
||||
notifications: [],
|
||||
userV: 'undefined'
|
||||
});
|
||||
|
||||
scope.applyCoupon('INVALID_COUPON');
|
||||
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(Notification.error).to.be.called;
|
||||
expect(Notification.error).to.be.calledWith(env.t('invalidCoupon'), true);
|
||||
});
|
||||
|
||||
it('displays an confirmation when a valid coupon is applied', function () {
|
||||
$httpBackend
|
||||
.whenPOST('/api/v3/coupons/validate/VALID_COUPON?userV=undefined')
|
||||
.respond(200, {
|
||||
success: true,
|
||||
data: {
|
||||
valid: true
|
||||
},
|
||||
notifications: [],
|
||||
userV: 'undefined'
|
||||
});
|
||||
|
||||
scope.applyCoupon('VALID_COUPON');
|
||||
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(Notification.error).to.not.be.called;
|
||||
expect(Notification.text).to.be.calledWith('Coupon applied!');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,14 +26,22 @@ describe('Analytics Service', function () {
|
||||
describe('register', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(window, 'fbq');
|
||||
sandbox.stub(amplitude, 'setUserId');
|
||||
sandbox.stub(window, 'ga');
|
||||
});
|
||||
|
||||
it('records a registration event on Facebook', function() {
|
||||
it('sets up user with Amplitude', function() {
|
||||
analytics.register();
|
||||
clock.tick();
|
||||
expect(fbq).to.have.been.calledOnce;
|
||||
expect(fbq).to.have.been.calledWith('track', 'CompleteRegistration');
|
||||
expect(amplitude.setUserId).to.have.been.calledOnce;
|
||||
expect(amplitude.setUserId).to.have.been.calledWith(user._id);
|
||||
});
|
||||
|
||||
it('sets up user with Google Analytics', function() {
|
||||
analytics.register();
|
||||
clock.tick();
|
||||
expect(ga).to.have.been.calledOnce;
|
||||
expect(ga).to.have.been.calledWith('set', {userId: user._id});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +74,6 @@ describe('Analytics Service', function () {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(amplitude, 'logEvent');
|
||||
sandbox.stub(window, 'ga');
|
||||
sandbox.stub(window, 'fbq');
|
||||
});
|
||||
|
||||
context('successful tracking', function() {
|
||||
@@ -106,15 +113,6 @@ describe('Analytics Service', function () {
|
||||
expect(ga).to.have.been.calledOnce;
|
||||
expect(ga).to.have.been.calledWith('send', properties);
|
||||
});
|
||||
|
||||
it('tracks a page view with Facebook', function() {
|
||||
var properties = {'hitType':'pageview','eventCategory':'behavior','eventAction':'tasks'};
|
||||
analytics.track(properties);
|
||||
clock.tick();
|
||||
|
||||
expect(fbq).to.have.been.calledOnce;
|
||||
expect(fbq).to.have.been.calledWith('track', 'PageView');
|
||||
});
|
||||
});
|
||||
|
||||
context('unsuccessful tracking', function() {
|
||||
|
||||
@@ -67,59 +67,6 @@ describe('Stats Service', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('classBonus', function() {
|
||||
it('calculates class bonus', function() {
|
||||
var equippedGear = {
|
||||
"weapon" : "weapon_warrior_1",
|
||||
"shield" : "shield_warrior_1",
|
||||
"head" : "head_warrior_1",
|
||||
"armor" : "armor_warrior_1"
|
||||
};
|
||||
var user = {
|
||||
fns: {
|
||||
statsComputed: function () {
|
||||
return { str: 50 };
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
lvl: 10,
|
||||
buffs: { str: 10 },
|
||||
str: 10
|
||||
},
|
||||
items: {
|
||||
gear: { equipped: equippedGear }
|
||||
}
|
||||
};
|
||||
var stat = 'str';
|
||||
var classBonus = statCalc.classBonus(user, stat);
|
||||
|
||||
expect(classBonus).to.eql(20)
|
||||
});
|
||||
|
||||
it('does not return value if user has not been wrapped (_statComputed)', function() {
|
||||
var equippedGear = {
|
||||
"weapon" : "weapon_warrior_1",
|
||||
"shield" : "shield_warrior_1",
|
||||
"head" : "head_warrior_1",
|
||||
"armor" : "armor_warrior_1"
|
||||
};
|
||||
var user = {
|
||||
stats: {
|
||||
lvl: 10,
|
||||
buffs: { str: 10 },
|
||||
str: 10
|
||||
},
|
||||
items: {
|
||||
gear: { equipped: equippedGear }
|
||||
}
|
||||
};
|
||||
var stat = 'str';
|
||||
var classBonus = statCalc.classBonus(user, stat);
|
||||
|
||||
expect(classBonus).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('expDisplay', function() {
|
||||
it('displays exp as "exp / toNextLevelExp"', function() {
|
||||
user.stats.exp = 10;
|
||||
@@ -138,27 +85,6 @@ describe('Stats Service', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('equipmentStatBonus', function() {
|
||||
it('tallies up stats from equipment that is equipped', function() {
|
||||
var equippedGear = {
|
||||
"weapon" : "weapon_special_1",
|
||||
"shield" : "shield_special_1",
|
||||
"head" : "head_special_1",
|
||||
"armor" : "armor_special_1"
|
||||
};
|
||||
|
||||
var strStat = statCalc.equipmentStatBonus('str', equippedGear);
|
||||
var conStat = statCalc.equipmentStatBonus('con', equippedGear);
|
||||
var intStat = statCalc.equipmentStatBonus('int', equippedGear);
|
||||
var perStat = statCalc.equipmentStatBonus('per', equippedGear);
|
||||
|
||||
expect(strStat).to.eql(24);
|
||||
expect(conStat).to.eql(24);
|
||||
expect(intStat).to.eql(24);
|
||||
expect(perStat).to.eql(24);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goldDisplay', function() {
|
||||
it('displays gold', function() {
|
||||
var gold = 30;
|
||||
@@ -192,32 +118,6 @@ describe('Stats Service', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('levelBonus', function() {
|
||||
it('calculates bonus as half of level for even numbered level under 100', function() {
|
||||
var level = 50;
|
||||
var bonus = statCalc.levelBonus(level);
|
||||
expect(bonus).to.eql(25);
|
||||
});
|
||||
|
||||
it('calculates bonus as half of level, rounded down, for odd numbered level under 100', function() {
|
||||
var level = 51;
|
||||
var bonus = statCalc.levelBonus(level);
|
||||
expect(bonus).to.eql(25);
|
||||
});
|
||||
|
||||
it('calculates bonus as 50 for levels >= 100', function() {
|
||||
var level = 150;
|
||||
var bonus = statCalc.levelBonus(level);
|
||||
expect(bonus).to.eql(50);
|
||||
});
|
||||
|
||||
it('calculates bonus as 0 for level 1', function() {
|
||||
var level = 1;
|
||||
var bonus = statCalc.levelBonus(level);
|
||||
expect(bonus).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mountMasterProgress', function() {
|
||||
it('counts drop mounts that user has', function() {
|
||||
user.items.mounts = {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"env": {
|
||||
"test": {
|
||||
plugins: [
|
||||
["istanbul"],
|
||||
],
|
||||
},
|
||||
},
|
||||
"presets": [
|
||||
["es2015", { modules: false }],
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"syntax-async-functions",
|
||||
"transform-regenerator",
|
||||
],
|
||||
"comments": false
|
||||
"comments": false,
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
require('babel-register');
|
||||
const config = require('../../../webpack/config');
|
||||
const chromeDriverPath = require('chromedriver').path;
|
||||
const seleniumServerPath = require('selenium-server').path;
|
||||
|
||||
// http://nightwatchjs.org/guide#settings-file
|
||||
module.exports = {
|
||||
@@ -12,7 +13,7 @@ module.exports = {
|
||||
|
||||
selenium: {
|
||||
start_process: true,
|
||||
server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
|
||||
server_path: seleniumServerPath,
|
||||
host: '127.0.0.1',
|
||||
port: 4444,
|
||||
cli_args: {
|
||||
|
||||
@@ -3,53 +3,12 @@
|
||||
// we are also using it with karma-webpack
|
||||
// https://github.com/webpack/karma-webpack
|
||||
|
||||
const path = require('path');
|
||||
const merge = require('webpack-merge');
|
||||
const baseConfig = require('../../../webpack/webpack.base.conf');
|
||||
const utils = require('../../../webpack/utils');
|
||||
const webpack = require('webpack');
|
||||
const projectRoot = path.resolve(__dirname, '../../../');
|
||||
const testEnv = require('../../../webpack/config/test.env');
|
||||
// Necessary for babel to respect the env version of .babelrc which is necessary
|
||||
// Because inject-loader does not work with ["es2015", { modules: false }] that we use
|
||||
// in order to let webpack2 handle the imports
|
||||
|
||||
const webpackConfig = merge(baseConfig, {
|
||||
// use inline sourcemap for karma-sourcemap-loader
|
||||
module: {
|
||||
loaders: utils.styleLoaders(),
|
||||
},
|
||||
devtool: '#inline-source-map',
|
||||
vue: {
|
||||
loaders: {
|
||||
js: 'isparta',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': testEnv,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// no need for app entry during tests
|
||||
delete webpackConfig.entry;
|
||||
|
||||
// make sure isparta loader is applied before eslint
|
||||
webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [];
|
||||
webpackConfig.module.preLoaders.unshift({
|
||||
test: /\.js$/,
|
||||
loader: 'isparta',
|
||||
include: [
|
||||
path.resolve(projectRoot, 'website/client'),
|
||||
path.resolve(projectRoot, 'website/common'),
|
||||
],
|
||||
});
|
||||
|
||||
// only apply babel for test files when using isparta
|
||||
webpackConfig.module.loaders.some((loader) => {
|
||||
if (loader.loader === 'babel') {
|
||||
loader.include = path.resolve(projectRoot, 'test/client/unit');
|
||||
return true;
|
||||
}
|
||||
});
|
||||
process.env.BABEL_ENV = process.env.NODE_ENV; // eslint-disable-line no-process-env
|
||||
const webpackConfig = require('../../../webpack/webpack.test.conf');
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import deepFreeze from 'client/libs/deepFreeze';
|
||||
|
||||
describe('deepFreeze', () => {
|
||||
it('works as expected', () => {
|
||||
let obj = {
|
||||
a: 1,
|
||||
b () {
|
||||
return this.a;
|
||||
},
|
||||
nested: {
|
||||
c: 2,
|
||||
nestedTwice: {
|
||||
d: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = deepFreeze(obj);
|
||||
expect(result).to.equal(obj);
|
||||
|
||||
expect(Object.isFrozen(obj)).to.equal(true);
|
||||
expect(Object.isFrozen(obj.nested)).to.equal(true);
|
||||
expect(Object.isFrozen(obj.nested.nestedTwice)).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import storeInjector from 'inject?-vue!client/store';
|
||||
import storeInjector from 'inject-loader?-vue!client/store';
|
||||
import { mapState, mapGetters, mapActions } from 'client/store';
|
||||
import { flattenAndNamespace } from 'client/store/helpers/internals';
|
||||
|
||||
|
||||
@@ -25,4 +25,71 @@ describe('common.fns.statsComputed', () => {
|
||||
expect(result.str).to.eql(0);
|
||||
expect(result.maxMP).to.eql(30);
|
||||
});
|
||||
|
||||
it('calculates stat bonuses for equipment', () => {
|
||||
user.items.gear.equipped.weapon = 'weapon_rogue_1';
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(2);
|
||||
expect(result.gearBonus.str).to.eql(2);
|
||||
});
|
||||
|
||||
it('calculates stat bonuses for class', () => {
|
||||
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(4.5);
|
||||
expect(result.gearBonus.str).to.eql(3);
|
||||
expect(result.classBonus.str).to.eql(1.5);
|
||||
});
|
||||
|
||||
it('calculates stat bonuses for level', () => {
|
||||
user.stats.lvl = 25;
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(12);
|
||||
expect(result.levelBonus.str).to.eql(12);
|
||||
});
|
||||
|
||||
it('correctly caps level stat bonuses', () => {
|
||||
user.stats.lvl = 150;
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(50);
|
||||
expect(result.levelBonus.str).to.eql(50);
|
||||
});
|
||||
|
||||
it('sets baseStat field', () => {
|
||||
user.stats.str = 20;
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(20);
|
||||
expect(result.baseStat.str).to.eql(20);
|
||||
});
|
||||
|
||||
it('sets buffs field', () => {
|
||||
user.stats.buffs.str = 150;
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.str).to.eql(150);
|
||||
expect(result.buff.str).to.eql(150);
|
||||
});
|
||||
|
||||
it('calculates mp from intelligence', () => {
|
||||
user.stats.int = 150;
|
||||
user.stats.buffs.int = 50;
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.maxMP).to.eql(430);
|
||||
});
|
||||
|
||||
it('calculates stat bonuses for back equipment', () => {
|
||||
user.items.gear.equipped.back = 'back_special_takeThis';
|
||||
let result = statsComputed(user);
|
||||
|
||||
expect(result.int).to.eql(1);
|
||||
expect(result.per).to.eql(1);
|
||||
expect(result.con).to.eql(1);
|
||||
expect(result.str).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,8 +55,8 @@ export async function resetHabiticaDB () {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'username@email.com',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
}, (insertErr) => {
|
||||
|
||||
@@ -19,10 +19,16 @@ module.exports = {
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css'],
|
||||
// Run the build command with an extra argument to
|
||||
// View the bundle analyzer report after build finishes:
|
||||
// `npm run build --report`
|
||||
// Set to `true` or `false` to always turn it on or off
|
||||
bundleAnalyzerReport: process.env.npm_config_report, // eslint-disable-line no-process-env
|
||||
},
|
||||
dev: {
|
||||
env: devEnv,
|
||||
port: 8080,
|
||||
autoOpenBrowser: true,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
staticAssetsDirectory,
|
||||
|
||||
@@ -2,5 +2,5 @@ const merge = require('webpack-merge');
|
||||
const devEnv = require('./dev.env');
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"',
|
||||
NODE_ENV: '"test"',
|
||||
});
|
||||
|
||||
@@ -4,8 +4,13 @@ const path = require('path');
|
||||
const express = require('express');
|
||||
const webpack = require('webpack');
|
||||
const config = require('./config');
|
||||
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV);
|
||||
}
|
||||
|
||||
const proxyMiddleware = require('http-proxy-middleware');
|
||||
const webpackConfig = process.env.NODE_ENV === 'testing' ?
|
||||
const webpackConfig = process.env.NODE_ENV === 'test' ?
|
||||
require('./webpack.prod.conf') :
|
||||
require('./webpack.dev.conf');
|
||||
|
||||
@@ -41,7 +46,7 @@ Object.keys(proxyTable).forEach((context) => {
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options };
|
||||
}
|
||||
app.use(proxyMiddleware(context, options));
|
||||
app.use(proxyMiddleware(options.filter || context, options));
|
||||
});
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
|
||||
@@ -11,7 +11,7 @@ exports.assetsPath = (_path) => {
|
||||
return path.posix.join(assetsSubDirectory, _path);
|
||||
};
|
||||
|
||||
exports.cssLoaders = (options) => {
|
||||
exports.cssLoaders = function cssLoaders (options) {
|
||||
options = options || {};
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loaders) {
|
||||
@@ -24,17 +24,22 @@ exports.cssLoaders = (options) => {
|
||||
loader = `${loader}-loader`;
|
||||
extraParamChar = '?';
|
||||
}
|
||||
return `${loader}${(options.sourceMap ? extraParamChar + 'sourceMap' : '')}`; // eslint-disable-line prefer-template
|
||||
return loader + (options.sourceMap ? `${extraParamChar}sourceMap` : '');
|
||||
}).join('!');
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract('vue-style-loader', sourceLoader);
|
||||
return ExtractTextPlugin.extract({
|
||||
use: sourceLoader,
|
||||
fallback: 'vue-style-loader',
|
||||
});
|
||||
} else {
|
||||
return ['vue-style-loader', sourceLoader].join('!');
|
||||
}
|
||||
}
|
||||
|
||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||
// http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(['css']),
|
||||
postcss: generateLoaders(['css']),
|
||||
|
||||
@@ -19,8 +19,12 @@ const baseConfig = {
|
||||
filename: '[name].js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.vue'],
|
||||
fallback: [path.join(__dirname, '../node_modules')],
|
||||
extensions: ['*', '.js', '.vue', '.json'],
|
||||
modules: [
|
||||
path.join(__dirname, '..', 'website'),
|
||||
path.join(__dirname, '..', 'test/client/unit'),
|
||||
path.join(__dirname, '..', 'node_modules'),
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery',
|
||||
website: path.resolve(__dirname, '../website'),
|
||||
@@ -30,9 +34,6 @@ const baseConfig = {
|
||||
components: path.resolve(__dirname, '../website/client/components'),
|
||||
},
|
||||
},
|
||||
resolveLoader: {
|
||||
fallback: [path.join(__dirname, '../node_modules')],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
$: 'jquery',
|
||||
@@ -40,38 +41,34 @@ const baseConfig = {
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
preLoaders: !IS_PROD ? [
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'eslint',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: IS_PROD ?
|
||||
config.build.productionSourceMap :
|
||||
config.dev.cssSourceMap,
|
||||
extract: IS_PROD,
|
||||
}),
|
||||
postcss: [
|
||||
autoprefixer({
|
||||
browsers: ['last 2 versions'],
|
||||
}),
|
||||
postcssEasyImport(),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'eslint',
|
||||
loader: 'babel-loader',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
] : [],
|
||||
loaders: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue',
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
|
||||
@@ -79,7 +76,7 @@ const baseConfig = {
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
|
||||
@@ -87,25 +84,21 @@ const baseConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
vue: {
|
||||
loaders: utils.cssLoaders(),
|
||||
postcss: [
|
||||
autoprefixer({
|
||||
browsers: ['last 2 versions'],
|
||||
}),
|
||||
postcssEasyImport({
|
||||
glob: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (!IS_PROD) {
|
||||
const eslintFriendlyFormatter = require('eslint-friendly-formatter'); // eslint-disable-line global-require
|
||||
|
||||
baseConfig.eslint = {
|
||||
formatter: eslintFriendlyFormatter,
|
||||
emitWarning: true,
|
||||
};
|
||||
baseConfig.module.rules.unshift({
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: projectRoot,
|
||||
options: {
|
||||
formatter: eslintFriendlyFormatter,
|
||||
emitWarning: true,
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
});
|
||||
}
|
||||
module.exports = baseConfig;
|
||||
@@ -12,16 +12,15 @@ Object.keys(baseWebpackConfig.entry).forEach((name) => {
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }),
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }),
|
||||
},
|
||||
// eval-source-map is faster for development
|
||||
devtool: '#eval-source-map',
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
|
||||
@@ -8,13 +8,13 @@ const merge = require('webpack-merge');
|
||||
const baseWebpackConfig = require('./webpack.base.conf');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const env = process.env.NODE_ENV === 'testing' ?
|
||||
const env = process.env.NODE_ENV === 'test' ?
|
||||
require('./config/test.env') :
|
||||
config.build.env;
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }),
|
||||
rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }),
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
@@ -22,12 +22,6 @@ const webpackConfig = merge(baseWebpackConfig, {
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
|
||||
},
|
||||
vue: {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true,
|
||||
}),
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
@@ -37,15 +31,17 @@ const webpackConfig = merge(baseWebpackConfig, {
|
||||
compress: {
|
||||
warnings: false,
|
||||
},
|
||||
sourceMap: true,
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css'),
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing' ?
|
||||
filename: process.env.NODE_ENV === 'test' ?
|
||||
'index.html' :
|
||||
config.build.index,
|
||||
template: './website/client/index.html',
|
||||
@@ -97,4 +93,9 @@ if (config.build.productionGzip) {
|
||||
);
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // eslint-disable-line global-require
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||
}
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// This is the webpack config used for unit tests.
|
||||
const merge = require('webpack-merge');
|
||||
const baseConfig = require('./webpack.base.conf');
|
||||
const utils = require('./utils');
|
||||
const webpack = require('webpack');
|
||||
const testEnv = require('./config/test.env');
|
||||
|
||||
const webpackConfig = merge(baseConfig, {
|
||||
// use inline sourcemap for karma-sourcemap-loader
|
||||
module: {
|
||||
rules: utils.styleLoaders(),
|
||||
},
|
||||
devtool: '#inline-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': testEnv,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// no need for app entry during tests
|
||||
delete webpackConfig.entry;
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1,9 +1,9 @@
|
||||
/* Comment out for holiday events */
|
||||
/* .npc_ian {
|
||||
.npc_ian {
|
||||
background: url("/npc_ian.gif") no-repeat;
|
||||
width: 78px;
|
||||
height: 135px;
|
||||
} */
|
||||
}
|
||||
|
||||
.quest_burnout {
|
||||
background: url("/quest_burnout.gif") no-repeat;
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
.promo_android {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -180px;
|
||||
background-position: -1547px -553px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201602 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -284px -1193px;
|
||||
background-position: -845px -1041px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201603 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -142px -1193px;
|
||||
background-position: -703px -1041px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201604 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1264px 0px;
|
||||
background-position: -282px -1041px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201605 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -846px -600px;
|
||||
background-position: -1124px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
@@ -42,45 +42,51 @@
|
||||
}
|
||||
.promo_backgrounds_armoire_201608 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1264px -442px;
|
||||
background-position: -142px -600px;
|
||||
width: 140px;
|
||||
height: 439px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201609 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1405px 0px;
|
||||
background-position: -283px -600px;
|
||||
width: 139px;
|
||||
height: 438px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201610 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -141px -600px;
|
||||
background-position: -1265px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201611 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -282px -600px;
|
||||
background-position: -141px -1041px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201612 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -423px -600px;
|
||||
background-position: -1406px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201701 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -564px -600px;
|
||||
background-position: -1124px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backtoschool {
|
||||
.promo_backgrounds_armoire_201702 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -433px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background-position: -982px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201703 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -840px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_burnout {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
@@ -90,139 +96,151 @@
|
||||
}
|
||||
.promo_chairs_glasses {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1651px -532px;
|
||||
background-position: 0px -1483px;
|
||||
width: 51px;
|
||||
height: 210px;
|
||||
}
|
||||
.promo_checkin_incentives {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1193px;
|
||||
background-position: -987px -1041px;
|
||||
width: 141px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_classes_fall_2014 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1488px;
|
||||
background-position: -1547px -452px;
|
||||
width: 321px;
|
||||
height: 100px;
|
||||
}
|
||||
.promo_classes_fall_2015 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -831px -1042px;
|
||||
background-position: -703px -1336px;
|
||||
width: 377px;
|
||||
height: 99px;
|
||||
}
|
||||
.promo_classes_fall_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1405px -439px;
|
||||
background-position: -1547px -103px;
|
||||
width: 103px;
|
||||
height: 348px;
|
||||
}
|
||||
.promo_coffee_mug {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px 0px;
|
||||
background-position: -1651px -103px;
|
||||
width: 200px;
|
||||
height: 179px;
|
||||
}
|
||||
.promo_contrib_spotlight_Keith {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1947px -1248px;
|
||||
background-position: -90px -1694px;
|
||||
width: 87px;
|
||||
height: 111px;
|
||||
}
|
||||
.promo_contrib_spotlight_beffymaroo {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1897px -433px;
|
||||
background-position: -1794px -729px;
|
||||
width: 114px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_contrib_spotlight_blade {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1636px -897px;
|
||||
background-position: 0px -1694px;
|
||||
width: 89px;
|
||||
height: 111px;
|
||||
}
|
||||
.promo_contrib_spotlight_cantras {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -1185px;
|
||||
background-position: -178px -1694px;
|
||||
width: 87px;
|
||||
height: 109px;
|
||||
}
|
||||
.promo_contrib_spotlight_megan {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -897px;
|
||||
background-position: -143px -1483px;
|
||||
width: 90px;
|
||||
height: 111px;
|
||||
}
|
||||
.promo_contrib_spotlight_shanaqui {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -52px -1483px;
|
||||
width: 90px;
|
||||
height: 111px;
|
||||
}
|
||||
.promo_cow {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1123px 0px;
|
||||
background-position: -1406px 0px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_cupid_potions {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -564px -1041px;
|
||||
width: 138px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_dilatoryDistress {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -728px -1678px;
|
||||
background-position: -1910px -970px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_egg_mounts {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -567px -1193px;
|
||||
background-position: -824px -600px;
|
||||
width: 280px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_enchanted_armoire {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -928px -1341px;
|
||||
background-position: -1081px -1336px;
|
||||
width: 374px;
|
||||
height: 76px;
|
||||
}
|
||||
.promo_enchanted_armoire_201507 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -974px;
|
||||
background-position: -853px -751px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201508 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1157px;
|
||||
background-position: -1270px -1133px;
|
||||
width: 180px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201509 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -182px -1678px;
|
||||
background-position: -1910px -1243px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201511 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1510px;
|
||||
background-position: -1270px -1224px;
|
||||
width: 122px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_enchanted_armoire_201601 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -987px -691px;
|
||||
background-position: -1910px -1425px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_floral_potions {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -532px;
|
||||
background-position: -1547px -729px;
|
||||
width: 105px;
|
||||
height: 273px;
|
||||
}
|
||||
.promo_ghost_potions {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -600px;
|
||||
background-position: -423px -1041px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_habitica {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -356px;
|
||||
background-position: -1723px -553px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
@@ -234,211 +252,211 @@
|
||||
}
|
||||
.promo_habitoween_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -982px 0px;
|
||||
background-position: -1265px -442px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_haunted_hair {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1846px -1248px;
|
||||
background-position: -840px -442px;
|
||||
width: 100px;
|
||||
height: 137px;
|
||||
}
|
||||
.promo_holly_potions {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -840px 0px;
|
||||
background-position: 0px -600px;
|
||||
width: 141px;
|
||||
height: 440px;
|
||||
}
|
||||
.promo_item_notif {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -182px;
|
||||
background-position: -1651px -283px;
|
||||
width: 249px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_mystery_201405 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -455px -1678px;
|
||||
background-position: -1910px -1698px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201406 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1405px -888px;
|
||||
background-position: -1910px -418px;
|
||||
width: 90px;
|
||||
height: 96px;
|
||||
}
|
||||
.promo_mystery_201407 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1703px -669px;
|
||||
background-position: -306px -536px;
|
||||
width: 42px;
|
||||
height: 62px;
|
||||
}
|
||||
.promo_mystery_201408 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1966px -1396px;
|
||||
background-position: -1469px -1041px;
|
||||
width: 60px;
|
||||
height: 71px;
|
||||
}
|
||||
.promo_mystery_201409 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -637px -1678px;
|
||||
background-position: -1910px -1607px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201410 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1666px -806px;
|
||||
background-position: -1836px -1351px;
|
||||
width: 72px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201411 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -819px -1678px;
|
||||
background-position: -1910px -1516px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201412 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1703px -602px;
|
||||
background-position: -1071px -751px;
|
||||
width: 42px;
|
||||
height: 66px;
|
||||
}
|
||||
.promo_mystery_201501 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1660px -1097px;
|
||||
background-position: -1852px -103px;
|
||||
width: 48px;
|
||||
height: 63px;
|
||||
}
|
||||
.promo_mystery_201502 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -699px -448px;
|
||||
background-position: -1910px -515px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201503 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -987px -600px;
|
||||
background-position: -1910px -1061px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201504 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1660px -1009px;
|
||||
background-position: -1456px -1336px;
|
||||
width: 60px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201505 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -546px -1678px;
|
||||
background-position: -1910px -788px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201506 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1703px -532px;
|
||||
background-position: -1852px -167px;
|
||||
width: 42px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_mystery_201507 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1636px -1295px;
|
||||
background-position: -1910px -106px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201508 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -840px -441px;
|
||||
background-position: -146px -1595px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201509 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -364px -1678px;
|
||||
background-position: -1910px -606px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201510 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -982px -442px;
|
||||
background-position: -240px -1595px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201511 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -273px -1678px;
|
||||
background-position: -428px -1595px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201512 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1945px -1065px;
|
||||
background-position: -1848px -1171px;
|
||||
width: 60px;
|
||||
height: 81px;
|
||||
}
|
||||
.promo_mystery_201601 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -806px;
|
||||
background-position: -1393px -1224px;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201602 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -91px -1678px;
|
||||
background-position: -1910px -1334px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201603 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1678px;
|
||||
background-position: -1910px -879px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201604 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1264px -882px;
|
||||
background-position: -52px -1595px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201605 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -987px -873px;
|
||||
background-position: -1910px -697px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201606 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -1295px;
|
||||
background-position: -1910px 0px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201607 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -987px -782px;
|
||||
background-position: -1910px -1152px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201608 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1927px -1157px;
|
||||
background-position: -334px -1595px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201609 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1123px -884px;
|
||||
background-position: -1451px -1133px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201610 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1964px -883px;
|
||||
background-position: -1836px -1260px;
|
||||
width: 63px;
|
||||
height: 84px;
|
||||
}
|
||||
.promo_mystery_201611 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1405px -788px;
|
||||
background-position: -1910px -318px;
|
||||
width: 90px;
|
||||
height: 99px;
|
||||
}
|
||||
@@ -448,207 +466,171 @@
|
||||
width: 558px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_mystery_201701 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1910px -212px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201702 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -703px -854px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_3014 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -883px;
|
||||
background-position: -1653px -877px;
|
||||
width: 217px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_new_hair_fall2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1123px -442px;
|
||||
background-position: 0px -1041px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_orca {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1860px -1396px;
|
||||
background-position: -983px -854px;
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_partyhats {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1601px;
|
||||
background-position: -1651px -386px;
|
||||
width: 115px;
|
||||
height: 47px;
|
||||
}
|
||||
.promo_pastel_skin {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1209px -1042px;
|
||||
background-position: -1547px -1087px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.customize-option.promo_pastel_skin {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1234px -1057px;
|
||||
background-position: -1572px -1102px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_peppermint_flame {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -735px;
|
||||
background-position: -1406px -884px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pet_skins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1887px -735px;
|
||||
background-position: -1653px -729px;
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_pet_skins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1912px -750px;
|
||||
background-position: -1678px -744px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_pyromancer {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1396px;
|
||||
background-position: -699px -448px;
|
||||
width: 113px;
|
||||
height: 113px;
|
||||
}
|
||||
.promo_rainbow_armor {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1633px -1185px;
|
||||
background-position: -266px -1694px;
|
||||
width: 92px;
|
||||
height: 103px;
|
||||
}
|
||||
.promo_seasonal_shop_fall_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -848px -1193px;
|
||||
background-position: -423px -854px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_shimmer_hair {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -322px -1488px;
|
||||
background-position: -1547px -1003px;
|
||||
width: 330px;
|
||||
height: 83px;
|
||||
}
|
||||
.promo_splashyskins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1065px;
|
||||
background-position: -1270px -1041px;
|
||||
width: 198px;
|
||||
height: 91px;
|
||||
}
|
||||
.customize-option.promo_splashyskins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1771px -1080px;
|
||||
background-position: -1295px -1056px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_spooky_sparkles_fall_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -426px -1193px;
|
||||
background-position: -1129px -1041px;
|
||||
width: 140px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_spring_classes_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1128px -1193px;
|
||||
background-position: -1547px 0px;
|
||||
width: 362px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_springclasses2014 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -91px;
|
||||
background-position: -1547px -1351px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_springclasses2015 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px 0px;
|
||||
background-position: -1547px -1260px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_staff_spotlight_Lemoness {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1897px -584px;
|
||||
background-position: -1124px -884px;
|
||||
width: 102px;
|
||||
height: 146px;
|
||||
}
|
||||
.promo_staff_spotlight_Viirus {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1903px -285px;
|
||||
background-position: -1265px -884px;
|
||||
width: 119px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_staff_spotlight_paglias {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -1248px;
|
||||
background-position: -982px -442px;
|
||||
width: 99px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_startingover {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -584px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_summer_classes_2014 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -401px -1042px;
|
||||
background-position: -423px -751px;
|
||||
width: 429px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_summer_classes_2015 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1589px;
|
||||
background-position: -1547px -1171px;
|
||||
width: 300px;
|
||||
height: 88px;
|
||||
}
|
||||
.promo_summer_classes_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1042px;
|
||||
background-position: -423px -600px;
|
||||
width: 400px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_takeThis_gear {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -1009px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_takethis_armor {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1545px -1097px;
|
||||
background-position: -234px -1483px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_turkey_day_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -705px -600px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1651px -743px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_unconventional_armor2 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1964px -974px;
|
||||
width: 70px;
|
||||
height: 74px;
|
||||
}
|
||||
.promo_updos {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1746px -285px;
|
||||
width: 156px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_veteran_pets {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -1869px -1510px;
|
||||
width: 146px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_winter_classes_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -567px -1341px;
|
||||
width: 360px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1016 KiB |
@@ -1,48 +1,96 @@
|
||||
.promo_takeThis_gear {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -643px -732px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.promo_task_planning {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -282px -536px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
}
|
||||
.promo_turkey_day_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: 0px -440px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -1108px -558px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_unconventional_armor2 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -1108px -483px;
|
||||
width: 70px;
|
||||
height: 74px;
|
||||
}
|
||||
.promo_updos {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -885px -655px;
|
||||
width: 156px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_veteran_pets {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -1038px -803px;
|
||||
width: 146px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_winter_classes_2016 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -282px -732px;
|
||||
width: 360px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_winter_classes_2017 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: 0px -220px;
|
||||
background-position: -452px 0px;
|
||||
width: 432px;
|
||||
height: 144px;
|
||||
}
|
||||
.promo_winter_fireworks {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -754px -373px;
|
||||
background-position: -1042px -655px;
|
||||
width: 138px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winterclasses2015 {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -452px -262px;
|
||||
background-position: -452px -326px;
|
||||
width: 325px;
|
||||
height: 110px;
|
||||
}
|
||||
.promo_wintery_skins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: 0px -365px;
|
||||
background-position: -141px -440px;
|
||||
width: 140px;
|
||||
height: 441px;
|
||||
}
|
||||
.customize-option.promo_wintery_skins {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -25px -380px;
|
||||
background-position: -166px -455px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_winteryhair {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -778px -262px;
|
||||
background-position: -885px -803px;
|
||||
width: 152px;
|
||||
height: 75px;
|
||||
}
|
||||
.avatar_variety {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -452px 0px;
|
||||
background-position: -282px -440px;
|
||||
width: 498px;
|
||||
height: 95px;
|
||||
}
|
||||
.npc_viirus {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -452px -524px;
|
||||
background-position: -453px -882px;
|
||||
width: 108px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -52,33 +100,69 @@
|
||||
width: 451px;
|
||||
height: 219px;
|
||||
}
|
||||
.scene_coding {
|
||||
.promo_backtoschool {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -452px -373px;
|
||||
background-position: 0px -882px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_cooking {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: 0px -220px;
|
||||
width: 396px;
|
||||
height: 219px;
|
||||
}
|
||||
.promo_startingover {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -151px -882px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.promo_valentines {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -523px -536px;
|
||||
width: 309px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_working_out {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -885px 0px;
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
}
|
||||
.scene_coding {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -302px -882px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.scene_eco_friendly {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -885px -483px;
|
||||
width: 222px;
|
||||
height: 171px;
|
||||
}
|
||||
.scene_phone_peek {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -603px -373px;
|
||||
background-position: -723px -145px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.welcome_basic_avatars {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -699px -96px;
|
||||
background-position: -885px -151px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
.welcome_promo_party {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -141px -365px;
|
||||
background-position: -452px -145px;
|
||||
width: 270px;
|
||||
height: 180px;
|
||||
}
|
||||
.welcome_sample_tasks {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||
background-position: -452px -96px;
|
||||
background-position: -885px -317px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 316 KiB |