Merge branch 'develop' into party-chat-translations

# Conflicts:
#	website/server/controllers/api-v3/quests.js
#	website/server/controllers/api-v3/tasks/groups.js
#	website/server/controllers/api-v3/user/spells.js
#	website/server/models/group.js
This commit is contained in:
Mateus Etto
2018-04-25 21:15:49 +09:00
953 changed files with 37528 additions and 30517 deletions

View File

@@ -1,8 +1,5 @@
FROM node:8 FROM node:8
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
RUN yarn global add npm@5
# Install global packages # Install global packages
RUN npm install -g gulp-cli mocha RUN npm install -g gulp-cli mocha

View File

@@ -11,16 +11,13 @@ ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleu
ENV NODE_ENV production ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
RUN yarn global add npm@5
# Install global packages # Install global packages
RUN npm install -g gulp-cli mocha RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies # Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg WORKDIR /usr/src/habitrpg
RUN git clone --branch v4.29.8 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg RUN git clone --branch v4.37.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install RUN npm install
RUN gulp build:prod --force RUN gulp build:prod --force

View File

@@ -98,9 +98,9 @@
}, },
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111", "ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : { "EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com", "COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com", "TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com" "PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
}, },
"LOGGLY" : { "LOGGLY" : {
"TOKEN" : "example-token", "TOKEN" : "example-token",

View File

@@ -0,0 +1,88 @@
var migrationName = '20171211_sanitize_emails.js';
var authorName = 'Julius'; // in case script author needs to know when their ...
var authorUuid = 'dd16c270-1d6d-44bd-b4f9-737342e79be6'; //... own data is done
/*
User creation saves email as lowercase, but updating an email did not.
Run this script to ensure all lowercased emails in db AFTER fix for updating emails is implemented.
This will fix inconsistent querying for an email when attempting to password reset.
*/
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) {
var query = {
'auth.local.email': /[A-Z]/
};
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)
'auth.local.email'
],
})
.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 push;
var set = {
'auth.local.email': user.auth.local.email.toLowerCase()
};
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -0,0 +1,52 @@
// @migrationName = 'MigrateGroupChat';
// @authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
// @authorUuid = ''; // ... own data is done
/*
* This migration move ass chat off of groups and into their own model
*/
import { model as Group } from '../../website/server/models/group';
import { model as Chat } from '../../website/server/models/chat';
async function moveGroupChatToModel (skip = 0) {
const groups = await Group.find({})
.limit(50)
.skip(skip)
.sort({ _id: -1 })
.exec();
if (groups.length === 0) {
console.log('End of groups');
process.exit();
}
const promises = groups.map(group => {
const chatpromises = group.chat.map(message => {
const newChat = new Chat();
Object.assign(newChat, message);
newChat._id = message.id;
newChat.groupId = group._id;
return newChat.save();
});
group.chat = [];
chatpromises.push(group.save());
return chatpromises;
});
const reducedPromises = promises.reduce((acc, curr) => {
acc = acc.concat(curr);
return acc;
}, []);
console.log(reducedPromises);
await Promise.all(reducedPromises);
moveGroupChatToModel(skip + 50);
}
module.exports = moveGroupChatToModel;

View File

@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer(); setUpServer();
// Replace this with your migration // Replace this with your migration
const processUsers = require('./20180125_clean_new_notifications.js'); const processUsers = require('./groups/migrate-chat.js');
processUsers(); processUsers();

View File

@@ -0,0 +1,117 @@
import each from 'lodash/each';
import keys from 'lodash/keys';
import content from '../../website/common/script/content/index';
const migrationName = 'full-stable.js';
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award users every extant pet and mount
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk');
let dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'profile.name': 'SabreCat',
};
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):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {
migration: migrationName,
};
each(keys(content.pets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.premiumPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.questPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.specialPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.mounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.premiumMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.questMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.specialMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -5,7 +5,7 @@ const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is do
/* /*
* Award this month's mystery items to subscribers * Award this month's mystery items to subscribers
*/ */
const MYSTERY_ITEMS = ['armor_mystery_201802', 'head_mystery_201802', 'shield_mystery_201802']; const MYSTERY_ITEMS = ['back_mystery_201803', 'head_mystery_201803'];
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk'); let monk = require('monk');

821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.31.0", "version": "4.38.0",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@slack/client": "^3.8.1", "@slack/client": "^3.8.1",
@@ -9,8 +9,8 @@
"amazon-payments": "^0.2.6", "amazon-payments": "^0.2.6",
"amplitude": "^3.5.0", "amplitude": "^3.5.0",
"apidoc": "^0.17.5", "apidoc": "^0.17.5",
"autoprefixer": "^8.1.0", "autoprefixer": "^8.2.0",
"aws-sdk": "^2.209.0", "aws-sdk": "^2.224.1",
"axios": "^0.18.0", "axios": "^0.18.0",
"axios-progress-bar": "^1.1.8", "axios-progress-bar": "^1.1.8",
"babel-core": "^6.0.0", "babel-core": "^6.0.0",
@@ -27,19 +27,19 @@
"babel-runtime": "^6.11.6", "babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2", "bcrypt": "^1.0.2",
"body-parser": "^1.15.0", "body-parser": "^1.15.0",
"bootstrap": "^4.0.0", "bootstrap": "^4.1.0",
"bootstrap-vue": "^2.0.0-rc.2", "bootstrap-vue": "^2.0.0-rc.6",
"compression": "^1.7.2", "compression": "^1.7.2",
"cookie-session": "^1.2.0", "cookie-session": "^1.2.0",
"coupon-code": "^0.4.5", "coupon-code": "^0.4.5",
"cross-env": "^5.1.4", "cross-env": "^5.1.4",
"css-loader": "^0.28.0", "css-loader": "^0.28.11",
"csv-stringify": "^2.0.4", "csv-stringify": "^2.1.0",
"cwait": "^1.1.1", "cwait": "^1.1.1",
"domain-middleware": "~0.1.0", "domain-middleware": "~0.1.0",
"express": "^4.16.3", "express": "^4.16.3",
"express-basic-auth": "^1.1.4", "express-basic-auth": "^1.1.4",
"express-validator": "^5.0.3", "express-validator": "^5.1.2",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2", "glob": "^7.1.2",
"got": "^8.3.0", "got": "^8.3.0",
@@ -50,23 +50,24 @@
"gulp.spritesmith": "^6.9.0", "gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0", "habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1", "hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1", "html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2", "image-size": "^0.6.2",
"in-app-purchase": "^1.8.9", "in-app-purchase": "^1.9.0",
"intro.js": "^2.6.0", "intro.js": "^2.6.0",
"jquery": ">=3.0.0", "jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0", "js2xmlparser": "^3.0.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"memwatch-next": "^0.3.0",
"merge-stream": "^1.0.0", "merge-stream": "^1.0.0",
"method-override": "^2.3.5", "method-override": "^2.3.5",
"moment": "^2.21.0", "moment": "^2.22.0",
"moment-recur": "^1.0.7", "moment-recur": "^1.0.7",
"mongoose": "^5.0.10", "mongoose": "^5.0.14",
"morgan": "^1.7.0", "morgan": "^1.7.0",
"nconf": "^0.10.0", "nconf": "^0.10.0",
"node-gcm": "^0.14.4", "node-gcm": "^0.14.4",
"node-sass": "^4.8.2", "node-sass": "^4.8.3",
"nodemailer": "^4.6.3", "nodemailer": "^4.6.4",
"ora": "^2.0.0", "ora": "^2.0.0",
"pageres": "^4.1.1", "pageres": "^4.1.1",
"passport": "^0.4.0", "passport": "^0.4.0",
@@ -74,33 +75,33 @@
"passport-google-oauth20": "1.0.0", "passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0", "paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1", "paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.14.1", "popper.js": "^1.14.3",
"postcss-easy-import": "^3.0.0", "postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0", "ps-tree": "^1.0.0",
"pug": "^2.0.1", "pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", "push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0", "pusher": "^1.3.0",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"sass-loader": "^6.0.7", "sass-loader": "^7.0.0",
"shelljs": "^0.8.1", "shelljs": "^0.8.1",
"stackimpact": "^1.2.1", "stackimpact": "^1.3.0",
"stripe": "^5.5.0", "stripe": "^5.8.0",
"superagent": "^3.4.3", "superagent": "^3.4.3",
"svg-inline-loader": "^0.8.0", "svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2", "svg-url-loader": "^2.3.2",
"svgo": "^1.0.5", "svgo": "^1.0.5",
"svgo-loader": "^2.1.0", "svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16", "universal-analytics": "^0.4.16",
"url-loader": "^0.6.2", "url-loader": "^1.0.0",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^3.0.1", "uuid": "^3.0.1",
"validator": "^9.4.1", "validator": "^9.4.1",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vue": "^2.5.16", "vue": "^2.5.16",
"vue-loader": "^14.2.1", "vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1", "vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0", "vue-router": "^3.0.0",
"vue-style-loader": "^4.0.2", "vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16", "vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.15.0", "vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
@@ -140,24 +141,25 @@
"apidoc": "gulp apidoc" "apidoc": "gulp apidoc"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-syntax-object-rest-spread": "^6.13.0", "@vue/test-utils": "^1.0.0-beta.13",
"babel-plugin-istanbul": "^4.1.6", "babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chalk": "^2.3.2", "chalk": "^2.3.2",
"chromedriver": "^2.36.0", "chromedriver": "^2.37.0",
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"cross-spawn": "^6.0.5", "cross-spawn": "^6.0.5",
"eslint": "^4.18.2", "eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0", "eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^3.0.0", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^1.3.0", "eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.2", "eslint-plugin-html": "^4.0.3",
"eslint-plugin-mocha": "^4.12.1", "eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6", "eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"http-proxy-middleware": "^0.17.0", "http-proxy-middleware": "^0.18.0",
"istanbul": "^1.1.0-alpha.1", "istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.0", "karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0", "karma-babel-preprocessor": "^7.0.0",
@@ -166,24 +168,24 @@
"karma-coverage": "^1.1.1", "karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0", "karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5", "karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.3", "karma-sinon-chai": "^1.3.4",
"karma-sinon-stub-promise": "^1.0.0", "karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32", "karma-spec-reporter": "0.0.32",
"karma-webpack": "^2.0.13", "karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0", "lcov-result-merger": "^2.0.0",
"mocha": "^5.0.4", "mocha": "^5.0.5",
"monk": "^6.0.5", "monk": "^6.0.5",
"nightwatch": "^0.9.20", "nightwatch": "^0.9.20",
"puppeteer": "^1.1.1", "puppeteer": "^1.3.0",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"selenium-server": "^3.11.0", "selenium-server": "^3.11.0",
"sinon": "^4.4.5", "sinon": "^4.5.0",
"sinon-chai": "^2.8.0", "sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0", "sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1", "webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^2.0.5", "webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.21.2" "webpack-hot-middleware": "^2.22.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"node-rdkafka": "^2.3.0" "node-rdkafka": "^2.3.0"

View File

@@ -151,7 +151,10 @@ describe('GET challenges/groups/:groupId', () => {
}); });
officialChallenge = await generateChallenge(user, group, { officialChallenge = await generateChallenge(user, group, {
official: true, categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
}); });
challenge = await generateChallenge(user, group); challenge = await generateChallenge(user, group);

View File

@@ -193,7 +193,10 @@ describe('GET challenges/user', () => {
}); });
officialChallenge = await generateChallenge(user, group, { officialChallenge = await generateChallenge(user, group, {
official: true, categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
}); });
challenge = await generateChallenge(user, group); challenge = await generateChallenge(user, group);
@@ -224,4 +227,61 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(1); expect(foundChallengeIndex).to.eql(1);
}); });
}); });
context('filters and paging', () => {
let user, guild, member;
const categories = [{
slug: 'newCat',
name: 'New Category',
}];
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
user = groupLeader;
guild = group;
member = members[0];
await user.update({balance: 20});
for (let i = 0; i < 11; i += 1) {
await generateChallenge(user, group); // eslint-disable-line
}
});
it('returns public guilds filtered by category', async () => {
const categoryChallenge = await generateChallenge(user, guild, {categories});
const challenges = await user.get(`/challenges/user?categories=${categories[0].slug}`);
expect(challenges[0]._id).to.eql(categoryChallenge._id);
expect(challenges.length).to.eql(1);
});
it('does not page challenges if page parameter is absent', async () => {
const challenges = await user.get('/challenges/user');
expect(challenges.length).to.be.above(11);
});
it('paginates challenges', async () => {
const challenges = await user.get('/challenges/user?page=0');
const challengesPaged = await user.get('/challenges/user?page=1&owned=owned');
expect(challenges.length).to.eql(10);
expect(challengesPaged.length).to.eql(2);
});
it('filters by owned', async () => {
const challenges = await member.get('/challenges/user?owned=owned');
expect(challenges.length).to.eql(0);
});
});
}); });

View File

@@ -53,16 +53,26 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
it('allows creator to delete a their message', async () => { it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`); await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array'); const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).to.not.include(nextMessage); const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
}); });
it('allows admin to delete another user\'s message', async () => { it('allows admin to delete another user\'s message', async () => {
await admin.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`); await admin.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array'); const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).to.not.include(nextMessage); const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
}); });
it('returns empty when previous message parameter is passed and the last message was deleted', async () => { it('returns empty when previous message parameter is passed and the last message was deleted', async () => {
@@ -71,9 +81,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
}); });
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => { it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`); const updatedChat = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id); expect(updatedChat[0].id).to.eql(message.id);
}); });
}); });
}); });

View File

@@ -23,14 +23,14 @@ describe('GET /groups/:groupId/chat', () => {
privacy: 'public', privacy: 'public',
}, { }, {
chat: [ chat: [
{text: 'Hello', flags: {}}, {text: 'Hello', flags: {}, id: 1},
{text: 'Welcome to the Guild', flags: {}}, {text: 'Welcome to the Guild', flags: {}, id: 2},
], ],
}); });
}); });
it('returns Guild chat', async () => { it('returns Guild chat', async () => {
let chat = await user.get(`/groups/${group._id}/chat`); const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat).to.eql(group.chat); expect(chat).to.eql(group.chat);
}); });

View File

@@ -11,7 +11,7 @@ import {
TAVERN_ID, TAVERN_ID,
} from '../../../../../website/server/models/group'; } from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils'; import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords'; import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords'; import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email'; import * as email from '../../../../../website/server/libs/email';
@@ -23,11 +23,11 @@ const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => { describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember; let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message'; let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE'; let testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
let testSlurMessage = 'message with TEST_PLACEHOLDER_SLUR_WORD_HERE'; let testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
let bannedWordErrorMessage = t('bannedWordUsed').split('.'); let testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`; let testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
bannedWordErrorMessage = bannedWordErrorMessage.join('.'); let bannedWordErrorMessage = t('bannedWordUsed', {swearWordsUsed: testBannedWordMessage});
before(async () => { before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({ let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -39,6 +39,7 @@ describe('POST /chat', () => {
members: 2, members: 2,
}); });
user = groupLeader; user = groupLeader;
await user.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group; groupWithChat = group;
member = members[0]; member = members[0];
additionalMember = members[1]; additionalMember = members[1];
@@ -136,9 +137,19 @@ describe('POST /chat', () => {
}); });
}); });
it('checks error message has the banned words used', async () => { it('errors when word is typed in mixed case', async () => {
let randIndex = Math.floor(Math.random() * (bannedWords.length + 1)); let substrLength = Math.floor(testBannedWordMessage.length / 2);
let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, '')); let chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase() + testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', {swearWordsUsed: chatMessage}),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
let testBannedWords = [testBannedWordMessage.toUpperCase(), testBannedWordMessage1.toLowerCase()];
let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`; let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage})) await expect(user.post('/groups/habitrpg/chat', { message: chatMessage}))
.to.eventually.be.rejected .to.eventually.be.rejected
@@ -320,6 +331,17 @@ describe('POST /chat', () => {
members[0].flags.chatRevoked = false; members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false}); await members[0].update({'flags.chatRevoked': false});
}); });
it('errors when slur is typed in mixed case', async () => {
let substrLength = Math.floor(testSlurMessage1.length / 2);
let chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase() + testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
}); });
it('does not error when sending a message to a private guild with a user with revoked chat', async () => { it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
@@ -359,9 +381,11 @@ describe('POST /chat', () => {
}); });
it('creates a chat', async () => { it('creates a chat', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage}); const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(message.message.id).to.exist; expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
}); });
it('creates a chat with user styles', async () => { it('creates a chat with user styles', async () => {

View File

@@ -7,8 +7,7 @@ import util from 'util';
let parseStringAsync = util.promisify(xml2js.parseString).bind(xml2js); let parseStringAsync = util.promisify(xml2js.parseString).bind(xml2js);
describe('GET /export/userdata.xml', () => { describe('GET /export/userdata.xml', () => {
// TODO disabled because it randomly causes the build to fail it('should return a valid XML file with user data', async () => {
xit('should return a valid XML file with user data', async () => {
let user = await generateUser(); let user = await generateUser();
let tasks = await user.post('/tasks/user', [ let tasks = await user.post('/tasks/user', [
{type: 'habit', text: 'habit 1'}, {type: 'habit', text: 'habit 1'},
@@ -31,13 +30,21 @@ describe('GET /export/userdata.xml', () => {
expect(res).to.contain.all.keys(['tasks', 'flags', 'tasksOrder', 'auth']); expect(res).to.contain.all.keys(['tasks', 'flags', 'tasksOrder', 'auth']);
expect(res.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(res.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(res.tasks).to.have.all.keys(['dailys', 'habits', 'todos', 'rewards']); expect(res.tasks).to.have.all.keys(['dailys', 'habits', 'todos', 'rewards']);
expect(res.tasks.habits.length).to.equal(2); expect(res.tasks.habits.length).to.equal(2);
expect(res.tasks.habits[0]._id).to.equal(tasks[0]._id); let habitIds = _.map(res.tasks.habits, '_id');
expect(habitIds).to.have.deep.members([tasks[0]._id, tasks[4]._id]);
expect(res.tasks.dailys.length).to.equal(2); expect(res.tasks.dailys.length).to.equal(2);
expect(res.tasks.dailys[0]._id).to.equal(tasks[1]._id); let dailyIds = _.map(res.tasks.dailys, '_id');
expect(dailyIds).to.have.deep.members([tasks[1]._id, tasks[5]._id]);
expect(res.tasks.rewards.length).to.equal(2); expect(res.tasks.rewards.length).to.equal(2);
expect(res.tasks.rewards[0]._id).to.equal(tasks[2]._id); let rewardIds = _.map(res.tasks.rewards, '_id');
expect(rewardIds).to.have.deep.members([tasks[2]._id, tasks[6]._id]);
expect(res.tasks.todos.length).to.equal(3); expect(res.tasks.todos.length).to.equal(3);
expect(res.tasks.todos[1]._id).to.equal(tasks[3]._id); let todoIds = _.map(res.tasks.todos, '_id');
expect(todoIds).to.deep.include.members([tasks[3]._id, tasks[7]._id]);
}); });
}); });

View File

@@ -201,8 +201,8 @@ describe('GET /groups', () => {
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1')) await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE); .to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2')) let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 3); // 1 created now, 3 by other tests .to.eventually.have.a.lengthOf(1 + 4); // 1 created now, 4 by other tests
expect(page2[3].name).to.equal('guild with less members'); expect(page2[4].name).to.equal('guild with less members');
}); });
}); });
@@ -220,4 +220,18 @@ describe('GET /groups', () => {
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern')) await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW); .to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
}); });
it('returns a list of groups user has access to', async () => {
let group = await generateGroup(user, {
name: 'c++ coders',
type: 'guild',
privacy: 'public',
});
// search for 'c++ coders'
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', group._id);
});
}); });

View File

@@ -72,7 +72,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]); ]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([ expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -93,7 +93,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]); ]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([ expect(Object.keys(memberRes.preferences).sort()).to.eql([

View File

@@ -136,6 +136,22 @@ describe('POST /group', () => {
}, },
}); });
}); });
it('returns an error when a user with no chat privileges attempts to create a public guild', async () => {
await user.update({ 'flags.chatRevoked': true });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
})
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotCreatePublicGuildWhenMuted'),
});
});
}); });
context('private guild', () => { context('private guild', () => {
@@ -163,6 +179,17 @@ describe('POST /group', () => {
}); });
}); });
it('creates a private guild when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
let privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
});
it('deducts gems from user and adds them to guild bank', async () => { it('deducts gems from user and adds them to guild bank', async () => {
let privateGuild = await user.post('/groups', { let privateGuild = await user.post('/groups', {
name: groupName, name: groupName,
@@ -201,6 +228,16 @@ describe('POST /group', () => {
}); });
}); });
it('creates a party when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
let party = await user.post('/groups', {
name: partyName,
type: partyType,
});
expect(party._id).to.exist;
});
it('does not require gems to create a party', async () => { it('does not require gems to create a party', async () => {
await user.update({ balance: 0 }); await user.update({ balance: 0 });

View File

@@ -44,12 +44,12 @@ describe('POST /group/:groupId/join', () => {
expect(res.leader.profile.name).to.eql(user.profile.name); expect(res.leader.profile.name).to.eql(user.profile.name);
}); });
it('returns an error is user was already a member', async () => { it('returns an error if user was already a member', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`); await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({ await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('userAlreadyInGroup'), message: t('youAreAlreadyInGroup'),
}); });
}); });
@@ -262,6 +262,30 @@ describe('POST /group/:groupId/join', () => {
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false); await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false);
}); });
it('does not allow user to leave a party if a quest was active and they were the only member', async () => {
let userToInvite = await generateUser();
let oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await userToInvite.update({
[`items.quests.${PET_QUEST}`]: 1,
});
await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageCannotLeaveWhileQuesting'),
});
});
it('invites joining member to active quest', async () => { it('invites joining member to active quest', async () => {
await user.update({ await user.update({
[`items.quests.${PET_QUEST}`]: 1, [`items.quests.${PET_QUEST}`]: 1,

View File

@@ -11,7 +11,7 @@ import {
each, each,
} from 'lodash'; } from 'lodash';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import * as payments from '../../../../../website/server/libs/payments'; import * as payments from '../../../../../website/server/libs/payments/payments';
describe('POST /groups/:groupId/leave', () => { describe('POST /groups/:groupId/leave', () => {
let typesOfGroups = { let typesOfGroups = {

View File

@@ -24,6 +24,19 @@ describe('Post /groups/:groupId/invite', () => {
}); });
describe('user id invites', () => { describe('user id invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user is not found', async () => { it('returns an error when invited user is not found', async () => {
let fakeID = generateUUID(); let fakeID = generateUUID();
@@ -160,6 +173,19 @@ describe('Post /groups/:groupId/invite', () => {
describe('email invites', () => { describe('email invites', () => {
let testInvite = {name: 'test', email: 'test@habitica.com'}; let testInvite = {name: 'test', email: 'test@habitica.com'};
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invite is missing an email', async () => { it('returns an error when invite is missing an email', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
emails: [{name: 'test'}], emails: [{name: 'test'}],
@@ -321,6 +347,19 @@ describe('Post /groups/:groupId/invite', () => {
}); });
describe('guild invites', () => { describe('guild invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user is already invited to the group', async () => { it('returns an error when invited user is already invited to the group', async () => {
let userToInvite = await generateUser(); let userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, { await inviter.post(`/groups/${group._id}/invite`, {
@@ -398,6 +437,19 @@ describe('Post /groups/:groupId/invite', () => {
}); });
}); });
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user has a pending invitation to the party', async () => { it('returns an error when invited user has a pending invitation to the party', async () => {
let userToInvite = await generateUser(); let userToInvite = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, { await inviter.post(`/groups/${party._id}/invite`, {

View File

@@ -32,7 +32,7 @@ describe('GET /members/:memberId', () => {
let memberRes = await user.get(`/members/${member._id}`); let memberRes = await user.get(`/members/${member._id}`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]); ]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([ expect(Object.keys(memberRes.preferences).sort()).to.eql([

View File

@@ -3,7 +3,7 @@ import {
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments : amazon #subscribeCancel', () => { describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel?noRedirect=true'; let endpoint = '/amazon/subscribe/cancel?noRedirect=true';

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #checkout', () => { describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout'; let endpoint = '/amazon/checkout';

View File

@@ -3,7 +3,7 @@ import {
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #subscribe', () => { describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe'; let endpoint = '/amazon/subscribe';

View File

@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments'; import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #cancelSubscribe', () => { describe('payments : apple #cancelSubscribe', () => {
let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true'; let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true';

View File

@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments'; import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #verify', () => { describe('payments : apple #verify', () => {
let endpoint = '/iap/ios/verify'; let endpoint = '/iap/ios/verify';

View File

@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3'; import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments'; import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #subscribe', () => { describe('payments : apple #subscribe', () => {
let endpoint = '/iap/ios/subscribe'; let endpoint = '/iap/ios/subscribe';

View File

@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments'; import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #cancelSubscribe', () => { describe('payments : google #cancelSubscribe', () => {
let endpoint = '/iap/android/subscribe/cancel?noRedirect=true'; let endpoint = '/iap/android/subscribe/cancel?noRedirect=true';

View File

@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3'; import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments'; import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #subscribe', () => { describe('payments : google #subscribe', () => {
let endpoint = '/iap/android/subscribe'; let endpoint = '/iap/android/subscribe';

View File

@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments'; import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #verify', () => { describe('payments : google #verify', () => {
let endpoint = '/iap/android/verify'; let endpoint = '/iap/android/verify';

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkout', () => { describe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout'; let endpoint = '/paypal/checkout';

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkoutSuccess', () => { describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success'; let endpoint = '/paypal/checkout/success';

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import shared from '../../../../../../website/common'; import shared from '../../../../../../website/common';
describe('payments : paypal #subscribe', () => { describe('payments : paypal #subscribe', () => {

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeCancel', () => { describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel'; let endpoint = '/paypal/subscribe/cancel';

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeSuccess', () => { describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success'; let endpoint = '/paypal/subscribe/success';

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments - paypal - #ipn', () => { describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn'; let endpoint = '/paypal/ipn';

View File

@@ -3,7 +3,7 @@ import {
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => { describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none'; let endpoint = '/stripe/subscribe/cancel?redirect=none';

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
generateGroup, generateGroup,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #checkout', () => { describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout'; let endpoint = '/stripe/checkout';

View File

@@ -3,7 +3,7 @@ import {
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeEdit', () => { describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit'; let endpoint = '/stripe/subscribe/edit';

View File

@@ -4,6 +4,7 @@ import {
generateUser, generateUser,
sleep, sleep,
} from '../../../../helpers/api-v3-integration.helper'; } from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/accept', () => { describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale'; const PET_QUEST = 'whale';
@@ -155,10 +156,11 @@ describe('POST /groups/:groupId/quests/accept', () => {
// quest will start after everyone has accepted // quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync(); const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist; expect(groupChat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']); expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`); let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined; expect(returnedGroup.chat[0]._meta).to.be.undefined;

View File

@@ -4,6 +4,7 @@ import {
generateUser, generateUser,
sleep, sleep,
} from '../../../../helpers/api-v3-integration.helper'; } from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/force-start', () => { describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale'; const PET_QUEST = 'whale';
@@ -241,11 +242,13 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await questingGroup.sync(); await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist; const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`); expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
const returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined; expect(returnedGroup.chat[0]._meta).to.be.undefined;
}); });
}); });

View File

@@ -5,6 +5,7 @@ import {
} from '../../../../helpers/api-v3-integration.helper'; } from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content'; import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/invite/:questKey', () => { describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup; let questingGroup;
@@ -199,11 +200,11 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`); await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync(); const groupChat = await Chat.find({ groupId: group._id }).exec();
expect(group.chat[0].text).to.exist; expect(groupChat[0].text).to.exist;
expect(group.chat[0]._meta).to.exist; expect(groupChat[0]._meta).to.exist;
expect(group.chat[0]._meta).to.have.all.keys(['participatingMembers']); expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await groupLeader.get(`/groups/${group._id}`); let returnedGroup = await groupLeader.get(`/groups/${group._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined; expect(returnedGroup.chat[0]._meta).to.be.undefined;

View File

@@ -90,7 +90,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let stub = sandbox.stub(Group.prototype, 'sendChat'); let stub = sandbox.spy(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`); let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([ await Promise.all([

View File

@@ -5,6 +5,7 @@ import {
sleep, sleep,
} from '../../../../helpers/api-v3-integration.helper'; } from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/reject', () => { describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup; let questingGroup;
@@ -185,11 +186,12 @@ describe('POST /groups/:groupId/quests/reject', () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`); await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`); await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist; const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']); expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`); let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined; expect(returnedGroup.chat[0]._meta).to.be.undefined;

View File

@@ -296,6 +296,16 @@ describe('PUT /tasks/:id', () => {
expect(fetchedDaily.text).to.eql('saved'); expect(fetchedDaily.text).to.eql('saved');
}); });
// This is a special case for iOS requests
it('will round a priority (difficulty)', async () => {
daily = await user.put(`/tasks/${daily._id}`, {
alias: 'alias',
priority: 0.10000000000005,
});
expect(daily.priority).to.eql(0.1);
});
}); });
context('habits', () => { context('habits', () => {

View File

@@ -34,6 +34,8 @@ describe('GET /user', () => {
expect(returnedUser._id).to.equal(user._id); expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist; expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist; expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist; expect(returnedUser.stats).to.not.exist;
}); });
}); });

View File

@@ -0,0 +1,24 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /user/in-app-rewards', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the reward items available for purchase', async () => {
let buyList = await user.get('/user/in-app-rewards');
expect(_.find(buyList, item => {
return item.text === t('armorWarrior1Text');
})).to.exist;
expect(_.find(buyList, item => {
return item.text === t('armorWarrior2Text');
})).to.not.exist;
});
});

View File

@@ -0,0 +1,27 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /user/toggle-pinned-item', () => {
let user;
before(async () => {
user = await generateUser();
});
it('cannot unpin potion', async () => {
await expect(user.get('/user/toggle-pinned-item/potion/potion'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cannotUnpinArmoirPotion'),
});
});
it('can pin shield_rogue_5', async () => {
let result = await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
expect(result.pinnedItems.length).to.be.eql(user.pinnedItems.length + 1);
});
});

View File

@@ -0,0 +1,148 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import getOfficialPinnedItems from '../../../../../website/common/script/libs/getOfficialPinnedItems.js';
describe('POST /user/move-pinned-item/:path/move/to/:position', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
beforeEach(async () => {
user = await generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
});
it('adjusts the order of pinned items with no order mismatch', async () => {
let testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
let testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/5');
await user.sync();
expect(user.pinnedItemsOrder[5]).to.equal('armoire');
expect(user.pinnedItemsOrder[2]).to.equal('gear.flat.weapon_warrior_1');
// We have done nothing to change pinnedItems!
expect(user.pinnedItems).to.deep.equal(testPinnedItems);
let expectedResponse = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'armoire',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
expectedResponse = expectedResponse.concat(officialPinnedItemPaths);
expect(res).to.eql(expectedResponse);
});
it('adjusts the order of pinned items with order mismatch', async () => {
let testPinnedItems = [
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/1');
await user.sync();
// The basic test
expect(user.pinnedItemsOrder[1]).to.equal('armoire');
// potion is now the last item because the 2 unacounted for cards show up
// at the beginning of the order
expect(user.pinnedItemsOrder[user.pinnedItemsOrder.length - 1]).to.equal('potion');
let expectedResponse = [
'cardTypes.thankyou',
'cardTypes.greeting',
'potion',
];
// inAppRewards is used here and will by default put these seasonal items in the front like this:
expectedResponse = officialPinnedItemPaths.concat(expectedResponse);
// now put "armoire" in where we moved it:
expectedResponse.splice(1, 0, 'armoire');
expect(res).to.eql(expectedResponse);
});
it('cannot move pinned item that you do not have pinned', async () => {
let testPinnedItems = [
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
try {
await user.post('/user/move-pinned-item/cardTypes.thankyou/move/to/1');
} catch (err) {
expect(err).to.exist;
}
});
});

View File

@@ -180,11 +180,42 @@ describe('POST /user/class/cast/:spellId', () => {
members: 1, members: 1,
}); });
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13}); await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth'); await groupLeader.post('/user/class/cast/earth');
await sleep(1); await sleep(1);
await group.sync(); const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system'); expect(groupMessages[0]).to.exist;
expect(groupMessages[0].uuid).to.equal('system');
});
it('Ethereal Surge does not recover mp of other mages', async () => {
let group = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 4,
});
let promises = [];
promises.push(group.groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[0].update({'stats.mp': 0, 'stats.class': 'warrior', 'stats.lvl': 20}));
promises.push(group.members[1].update({'stats.mp': 0, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[2].update({'stats.mp': 0, 'stats.class': 'rogue', 'stats.lvl': 20}));
promises.push(group.members[3].update({'stats.mp': 0, 'stats.class': 'healer', 'stats.lvl': 20}));
await Promise.all(promises);
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
}); });
it('cast bulk', async () => { it('cast bulk', async () => {
@@ -197,7 +228,7 @@ describe('POST /user/class/cast/:spellId', () => {
await groupLeader.post('/user/class/cast/earth', {quantity: 2}); await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1); await sleep(1);
await group.sync(); group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist; expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system'); expect(group.chat[0].uuid).to.equal('system');
@@ -258,11 +289,31 @@ describe('POST /user/class/cast/:spellId', () => {
expect(user.achievements.birthday).to.equal(1); expect(user.achievements.birthday).to.equal(1);
}); });
it('passes correct target to spell when targetType === \'task\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
expect(result.task._id).to.equal(task._id);
});
it('passes correct target to spell when targetType === \'self\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
let result = await user.post('/user/class/cast/frost');
expect(result.user.stats.mp).to.equal(10);
});
// TODO find a way to have sinon working in integration tests // TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server // it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'task\'');
it('passes correct target to spell when targetType === \'tasks\''); it('passes correct target to spell when targetType === \'tasks\'');
it('passes correct target to spell when targetType === \'self\'');
it('passes correct target to spell when targetType === \'party\''); it('passes correct target to spell when targetType === \'party\'');
it('passes correct target to spell when targetType === \'user\''); it('passes correct target to spell when targetType === \'user\'');
it('passes correct target to spell when targetType === \'party\' and user is not in a party'); it('passes correct target to spell when targetType === \'party\' and user is not in a party');

View File

@@ -30,10 +30,12 @@ describe('POST /user/release-both', () => {
'items.currentPet': animal, 'items.currentPet': animal,
'items.pets': loadPets(), 'items.pets': loadPets(),
'items.mounts': loadMounts(), 'items.mounts': loadMounts(),
'achievements.triadBingo': true,
}); });
}); });
it('returns an error when user balance is too low and user does not have triadBingo', async () => { // @TODO: Traid is now free. Add this back if we need
xit('returns an error when user balance is too low and user does not have triadBingo', async () => {
await expect(user.post('/user/release-both')) await expect(user.post('/user/release-both'))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
code: 401, code: 401,
@@ -45,9 +47,7 @@ describe('POST /user/release-both', () => {
// More tests in common code unit tests // More tests in common code unit tests
it('grants triad bingo with gems', async () => { it('grants triad bingo with gems', async () => {
await user.update({ await user.update();
balance: 1.5,
});
let response = await user.post('/user/release-both'); let response = await user.post('/user/release-both');
await user.sync(); await user.sync();

View File

@@ -27,6 +27,33 @@ describe('PUT /user', () => {
expect(user.stats.hp).to.eql(14); expect(user.stats.hp).to.eql(14);
}); });
it('tags must be an array', async () => {
await expect(user.put('/user', {
tags: {
tag: true,
},
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
});
});
it('update tags', async () => {
let userTags = user.tags;
await user.put('/user', {
tags: [...user.tags, {
name: 'new tag',
}],
});
await user.sync();
expect(user.tags.length).to.be.eql(userTags.length + 1);
});
it('profile.name cannot be an empty string or null', async () => { it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', { await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed 'profile.name': ' ', // string should be trimmed

View File

@@ -357,6 +357,21 @@ describe('POST /user/auth/local/register', () => {
}); });
}); });
it('sanitizes email params to a lowercase string before creating the user', async () => {
let username = generateRandomUserName();
let email = 'ISANEmAiL@ExAmPle.coM';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.email).to.equal(email.toLowerCase());
});
it('fails on a habitica.com email', async () => { it('fails on a habitica.com email', async () => {
let username = generateRandomUserName(); let username = generateRandomUserName();
let email = `${username}@habitica.com`; let email = `${username}@habitica.com`;

View File

@@ -13,7 +13,7 @@ import nconf from 'nconf';
const ENDPOINT = '/user/auth/update-email'; const ENDPOINT = '/user/auth/update-email';
describe('PUT /user/auth/update-email', () => { describe('PUT /user/auth/update-email', () => {
let newEmail = 'some-new-email_2@example.net'; let newEmail = 'SOmE-nEw-emAIl_2@example.net';
let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
context('Local Authenticaion User', async () => { context('Local Authenticaion User', async () => {
@@ -53,14 +53,15 @@ describe('PUT /user/auth/update-email', () => {
}); });
it('changes email if new email and existing password are provided', async () => { it('changes email if new email and existing password are provided', async () => {
let lowerCaseNewEmail = newEmail.toLowerCase();
let response = await user.put(ENDPOINT, { let response = await user.put(ENDPOINT, {
newEmail, newEmail,
password: oldPassword, password: oldPassword,
}); });
expect(response).to.eql({ email: 'some-new-email_2@example.net' }); expect(response.email).to.eql(lowerCaseNewEmail);
await user.sync(); await user.sync();
expect(user.auth.local.email).to.eql(newEmail); expect(user.auth.local.email).to.eql(lowerCaseNewEmail);
}); });
it('rejects if email is already taken', async () => { it('rejects if email is already taken', async () => {

View File

@@ -32,4 +32,11 @@ describe('GET /world-state', () => {
}, },
}); });
}); });
it('returns a string representing the current season for NPC sprites', async () => {
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('npcImageSuffix');
expect(res.npcImageSuffix).to.be.a('string');
});
}); });

View File

@@ -23,7 +23,7 @@ describe('cron', () => {
local: { local: {
username: 'username', username: 'username',
lowerCaseUsername: 'username', lowerCaseUsername: 'username',
email: 'email@email.email', email: 'email@example.com',
salt: 'salt', salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase hashed_password: 'hashed_password', // eslint-disable-line camelcase
}, },
@@ -82,7 +82,7 @@ describe('cron', () => {
}); });
it('does not reset plan.gemsBought within the month', () => { it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix()); let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate(); user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
@@ -117,21 +117,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(1); expect(user.purchased.plan.consecutive.offset).to.equal(1);
}); });
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
});
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => { it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate(); user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate(); user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
@@ -143,21 +128,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.trinkets).to.equal(1); expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
}); });
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => { it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25; user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5; user.purchased.plan.consecutive.count = 5;
@@ -184,6 +154,465 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.count).to.equal(0); expect(user.purchased.plan.consecutive.count).to.equal(0);
expect(user.purchased.plan.consecutive.offset).to.equal(0); expect(user.purchased.plan.consecutive.offset).to.equal(0);
}); });
describe('for a 1-month recurring subscription', () => {
let clock;
// create a user that will be used for all of these tests without a reset before each
let user1 = new User({
auth: {
local: {
username: 'username1',
lowerCaseUsername: 'username1',
email: 'email1@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user1 has a 1-month recurring subscription starting today
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
it('does not increment consecutive benefits after the first month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('does not increment consecutive benefits after the second month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('increments consecutive benefits after the third month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(3);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits after the fourth month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(4);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
});
describe('for a 3-month recurring subscription', () => {
let clock;
let user3 = new User({
auth: {
local: {
username: 'username3',
lowerCaseUsername: 'username3',
email: 'email3@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3 has a 3-month recurring subscription starting today
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(2);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(3);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(4);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(5);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(6);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(7);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
});
describe('for a 6-month recurring subscription', () => {
let clock;
let user6 = new User({
auth: {
local: {
username: 'username6',
lowerCaseUsername: 'username6',
email: 'email6@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6 has a 6-month recurring subscription starting today
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(6);
expect(user6.purchased.plan.consecutive.offset).to.equal(0);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(7);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(13);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(6);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(19);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 12-month recurring subscription', () => {
let clock;
let user12 = new User({
auth: {
local: {
username: 'username12',
lowerCaseUsername: 'username12',
email: 'email12@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user12 has a 12-month recurring subscription starting today
user12.purchased.plan.customerId = 'subscribedId';
user12.purchased.plan.dateUpdated = moment().toDate();
user12.purchased.plan.planId = 'basic_12mo';
user12.purchased.plan.consecutive.count = 0;
user12.purchased.plan.consecutive.offset = 12;
user12.purchased.plan.consecutive.trinkets = 4;
user12.purchased.plan.consecutive.gemCapExtra = 20;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(12);
expect(user12.purchased.plan.consecutive.offset).to.equal(0);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(13);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(25);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(12);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(37);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(16);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 3-month gift subscription (non-recurring)', () => {
let clock;
let user3g = new User({
auth: {
local: {
username: 'username3g',
lowerCaseUsername: 'username3g',
email: 'email3g@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3g has a 3-month gift subscription starting today
user3g.purchased.plan.customerId = 'Gift';
user3g.purchased.plan.dateUpdated = moment().toDate();
user3g.purchased.plan.dateTerminated = moment().add(3, 'months').toDate();
user3g.purchased.plan.planId = null;
user3g.purchased.plan.consecutive.count = 0;
user3g.purchased.plan.consecutive.offset = 3;
user3g.purchased.plan.consecutive.trinkets = 1;
user3g.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.consecutive.offset).to.equal(2);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(2);
expect(user3g.purchased.plan.consecutive.offset).to.equal(1);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(3);
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(0); // subscription has been erased by now
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased
clock.restore();
});
});
describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => {
let clock;
let user6x = new User({
auth: {
local: {
username: 'username6x',
lowerCaseUsername: 'username6x',
email: 'email6x@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6x has a 6-month recurring subscription starting 8 months in the past before issue #4819 was fixed
user6x.purchased.plan.customerId = 'subscribedId';
user6x.purchased.plan.dateUpdated = moment().toDate();
user6x.purchased.plan.planId = 'basic_6mo';
user6x.purchased.plan.consecutive.count = 8;
user6x.purchased.plan.consecutive.offset = 0;
user6x.purchased.plan.consecutive.trinkets = 3;
user6x.purchased.plan.consecutive.gemCapExtra = 15;
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(9);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(10);
expect(user6x.purchased.plan.consecutive.offset).to.equal(4);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(11);
expect(user6x.purchased.plan.consecutive.offset).to.equal(3);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(15);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(7);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
}); });
describe('end of the month perks when user is not subscribed', () => { describe('end of the month perks when user is not subscribed', () => {
@@ -1348,7 +1777,7 @@ describe('recoverCron', () => {
local: { local: {
username: 'username', username: 'username',
lowerCaseUsername: 'username', lowerCaseUsername: 'username',
email: 'email@email.email', email: 'email@example.com',
salt: 'salt', salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase hashed_password: 'hashed_password', // eslint-disable-line camelcase
}, },

View File

@@ -4,8 +4,8 @@ import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers'; import { createNonLeaderGroupMember } from '../paymentHelpers';

View File

@@ -1,6 +1,6 @@
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon'; import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import amzLib from '../../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group'; import { model as Group } from '../../../../../../../website/server/models/group';
import amzLib from '../../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
describe('#upgradeGroupPlan', () => { describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString; let spy, data, user, group, uuidString;

View File

@@ -1,10 +1,10 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases'; import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments'; import payments from '../../../../../../website/server/libs/payments/payments';
import applePayments from '../../../../../website/server/libs/applePayments'; import applePayments from '../../../../../../website/server/libs/payments/apple';
import iap from '../../../../../website/server/libs/inAppPurchases'; import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user'; import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../website/common'; import common from '../../../../../../website/common';
import moment from 'moment'; import moment from 'moment';
const i18n = common.i18n; const i18n = common.i18n;
@@ -57,6 +57,18 @@ describe('Apple Payments', () => {
}); });
}); });
it('should throw an error if getPurchaseData is invalid', async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
it('errors if the user cannot purchase gems', async () => { it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers)) await expect(applePayments.verifyGemPurchase(user, receipt, headers))
@@ -69,27 +81,76 @@ describe('Apple Payments', () => {
user.canGetGems.restore(); user.canGetGems.restore();
}); });
it('purchases gems', async () => { it('errors if amount does not exist', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers); iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: 'badProduct',
transactionId: token,
}]);
expect(iapSetupStub).to.be.calledOnce; await expect(applePayments.verifyGemPurchase(user, receipt, headers))
expect(iapValidateStub).to.be.calledOnce; .to.eventually.be.rejected.and.to.eql({
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); httpCode: 401,
expect(iapIsValidatedStub).to.be.calledOnce; name: 'NotAuthorized',
expect(iapIsValidatedStub).to.be.calledWith({}); message: applePayments.constants.RESPONSE_INVALID_ITEM,
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,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore(); user.canGetGems.restore();
}); });
const gemsCanPurchase = [
{
productId: 'com.habitrpg.ios.Habitica.4gems',
amount: 1,
},
{
productId: 'com.habitrpg.ios.Habitica.20gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.21gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.42gems',
amount: 10.5,
},
{
productId: 'com.habitrpg.ios.Habitica.84gems',
amount: 21,
},
];
gemsCanPurchase.forEach(gemTest => {
it(`purchases ${gemTest.productId} gems`, async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemTest.productId,
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
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: gemTest.amount,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
}); });
describe('subscribe', () => { describe('subscribe', () => {
@@ -133,7 +194,16 @@ describe('Apple Payments', () => {
iapModule.validate.restore(); iapModule.validate.restore();
iapModule.isValidated.restore(); iapModule.isValidated.restore();
iapModule.getPurchaseData.restore(); iapModule.getPurchaseData.restore();
payments.createSubscription.restore(); if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
}); });
it('should throw an error if receipt is invalid', async () => { it('should throw an error if receipt is invalid', async () => {
@@ -149,26 +219,69 @@ describe('Apple Payments', () => {
}); });
}); });
it('creates a user subscription', async () => { const subOptions = [
{
sku: 'subscription1month',
subKey: 'basic_earned',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.3month',
subKey: 'basic_3mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.6month',
subKey: 'basic_6mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.12month',
subKey: 'basic_12mo',
},
];
subOptions.forEach(option => {
it(`creates a user subscription for ${option.sku}`, async () => {
iapModule.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({day: 1}).toDate(),
productId: option.sku,
transactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.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,
});
});
});
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing); await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce; await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
expect(iapValidateStub).to.be.calledOnce; .to.eventually.be.rejected.and.to.eql({
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); httpCode: 401,
expect(iapIsValidatedStub).to.be.calledOnce; name: 'NotAuthorized',
expect(iapIsValidatedStub).to.be.calledWith({}); message: applePayments.constants.RESPONSE_ALREADY_USED,
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,
});
}); });
}); });

View File

@@ -1,10 +1,10 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases'; import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments'; import payments from '../../../../../../website/server/libs/payments/payments';
import googlePayments from '../../../../../website/server/libs/googlePayments'; import googlePayments from '../../../../../../website/server/libs/payments/google';
import iap from '../../../../../website/server/libs/inAppPurchases'; import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user'; import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../website/common'; import common from '../../../../../../website/common';
import moment from 'moment'; import moment from 'moment';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -1,7 +1,7 @@
import moment from 'moment'; import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email'; import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments'; import * as api from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group'; import { model as Group } from '../../../../../../../website/server/models/group';
import { import {

View File

@@ -3,10 +3,10 @@ import stripeModule from 'stripe';
import nconf from 'nconf'; import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email'; import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments'; import * as api from '../../../../../../../website/server/libs/payments/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments'; import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group'; import { model as Group } from '../../../../../../../website/server/models/group';
import { import {

View File

@@ -1,14 +1,14 @@
import moment from 'moment'; import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email'; import * as sender from '../../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments'; import * as api from '../../../../../../website/server/libs/payments/payments';
import analytics from '../../../../../website/server/libs/analyticsService'; import analytics from '../../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications'; import notifications from '../../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-v3-integration.helper'; import { translate as t } from '../../../../../helpers/api-v3-integration.helper';
import { import {
generateGroup, generateGroup,
} from '../../../../helpers/api-unit.helper.js'; } from '../../../../../helpers/api-unit.helper.js';
describe('payments/index', () => { describe('payments/index', () => {
let user, group, data, plan; let user, group, data, plan;

View File

@@ -1,6 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
describe('checkout success', () => { describe('checkout success', () => {

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import nconf from 'nconf'; import nconf from 'nconf';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';

View File

@@ -1,6 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import { import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';

View File

@@ -1,6 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import { import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';

View File

@@ -1,6 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import { import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';

View File

@@ -2,7 +2,7 @@
import moment from 'moment'; import moment from 'moment';
import cc from 'coupon-code'; import cc from 'coupon-code';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as Coupon } from '../../../../../../../website/server/models/coupon'; import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';

View File

@@ -4,8 +4,8 @@ import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -6,8 +6,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon'; import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -1,8 +1,8 @@
import stripeModule from 'stripe'; import stripeModule from 'stripe';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -4,7 +4,7 @@ import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
const i18n = common.i18n; const i18n = common.i18n;

View File

@@ -4,8 +4,8 @@ import {
generateGroup, generateGroup,
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common'; import common from '../../../../../../../website/common';
import logger from '../../../../../../../website/server/libs/logger'; import logger from '../../../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';

View File

@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js'; } from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user'; import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group'; import { model as Group } from '../../../../../../../website/server/models/group';
import stripePayments from '../../../../../../../website/server/libs/stripePayments'; import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments'; import payments from '../../../../../../../website/server/libs/payments/payments';
describe('Stripe - Upgrade Group Plan', () => { describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test'); const stripe = stripeModule('test');

View File

@@ -0,0 +1,40 @@
import {
generateRes,
generateReq,
} from '../../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res, req, user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
});
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory(false, {
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
done();
});
});
});
});

View File

@@ -182,7 +182,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader); await party.startQuest(questLeader);
await party.save(); await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat'); sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
}); });
afterEach(() => sendChatStub.restore()); afterEach(() => sendChatStub.restore());
@@ -378,7 +378,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader); await party.startQuest(questLeader);
await party.save(); await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat'); sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
}); });
afterEach(() => sendChatStub.restore()); afterEach(() => sendChatStub.restore());
@@ -1118,21 +1118,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'update'); sandbox.spy(User, 'update');
}); });
it('puts message at top of chat array', () => {
let oldMessage = {
text: 'a message',
};
party.chat.push(oldMessage, oldMessage, oldMessage);
party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }});
expect(party.chat).to.have.a.lengthOf(4);
expect(party.chat[0].text).to.eql('a new message');
expect(party.chat[0].uuid).to.eql('user-id');
});
it('formats message', () => { it('formats message', () => {
party.sendChat('a new message', { const chatMessage = party.sendChat('a new message', {
_id: 'user-id', _id: 'user-id',
profile: { name: 'user name' }, profile: { name: 'user name' },
contributor: { contributor: {
@@ -1147,11 +1134,11 @@ describe('Group Model', () => {
}, },
}); });
let chat = party.chat[0]; const chat = chatMessage;
expect(chat.text).to.eql('a new message'); expect(chat.text).to.eql('a new message');
expect(validator.isUUID(chat.id)).to.eql(true); expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number'); expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({}); expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({}); expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0); expect(chat.flagCount).to.eql(0);
@@ -1162,13 +1149,11 @@ describe('Group Model', () => {
}); });
it('formats message as system if no user is passed in', () => { it('formats message as system if no user is passed in', () => {
party.sendChat('a system message'); const chat = party.sendChat('a system message');
let chat = party.chat[0];
expect(chat.text).to.eql('a system message'); expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true); expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number'); expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({}); expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({}); expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0); expect(chat.flagCount).to.eql(0);
@@ -1575,7 +1560,8 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1); expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
}); });
it('gives out super awesome Masterclasser achievement to the deserving', async () => { // Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4; quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key; party.quest.key = quest.key;
@@ -1603,8 +1589,45 @@ describe('Group Model', () => {
updatedLeader, updatedLeader,
updatedParticipatingMember, updatedParticipatingMember,
] = await Promise.all([ ] = await Promise.all([
User.findById(questLeader._id), User.findById(questLeader._id).exec(),
User.findById(participatingMember._id), User.findById(participatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
questLeader.achievements.quests = {
mayhemMistiflying1: 1,
mayhemMistiflying2: 1,
mayhemMistiflying3: 1,
stoikalmCalamity1: 1,
stoikalmCalamity2: 1,
stoikalmCalamity3: 1,
taskwoodsTerror1: 1,
taskwoodsTerror2: 1,
taskwoodsTerror3: 1,
dilatoryDistress1: 1,
dilatoryDistress2: 1,
dilatoryDistress3: 1,
lostMasterclasser2: 1,
lostMasterclasser3: 1,
lostMasterclasser4: 1,
};
await questLeader.save();
await party.finishQuest(quest);
let [
updatedLeader,
updatedParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
]); ]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true); expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);

View File

@@ -0,0 +1,211 @@
import { shallow, createLocalVue } from '@vue/test-utils';
import TaskColumn from 'client/components/tasks/column.vue';
import Store from 'client/libs/store';
// eslint-disable no-exclusive-tests
const localVue = createLocalVue();
localVue.use(Store);
describe('Task Column', () => {
let wrapper;
let store, getters;
let habits, taskListOverride, tasks;
function makeWrapper (additionalSetup = {}) {
let type = 'habit';
let mocks = {
$t () {},
};
let stubs = ['b-modal']; // <b-modal> is a custom component and not tested here
return shallow(TaskColumn, {
propsData: {
type,
},
mocks,
stubs,
localVue,
...additionalSetup,
});
}
it('returns a vue instance', () => {
wrapper = makeWrapper();
expect(wrapper.isVueInstance()).to.be.true;
});
describe('Passed Properties', () => {
beforeEach(() => {
wrapper = makeWrapper();
});
it('defaults isUser to false', () => {
expect(wrapper.vm.isUser).to.be.false;
});
it('passes isUser to component instance', () => {
wrapper.setProps({ isUser: false });
expect(wrapper.vm.isUser).to.be.false;
wrapper.setProps({ isUser: true });
expect(wrapper.vm.isUser).to.be.true;
});
});
describe('Computed Properties', () => {
beforeEach(() => {
habits = [
{ id: 1 },
{ id: 2 },
];
taskListOverride = [
{ id: 3 },
{ id: 4 },
];
getters = {
// (...) => { ... } will return a value
// (...) => (...) => { ... } will return a function
// Task Column expects a function
'tasks:getFilteredTaskList': () => () => habits,
};
store = new Store({getters});
wrapper = makeWrapper({store});
});
it('returns task list from props for group-plan', () => {
wrapper.setProps({ taskListOverride });
wrapper.vm.taskList.forEach((el, i) => {
expect(el).to.eq(taskListOverride[i]);
});
wrapper.setProps({ isUser: false, taskListOverride });
wrapper.vm.taskList.forEach((el, i) => {
expect(el).to.eq(taskListOverride[i]);
});
});
it('returns task list from store for user', () => {
wrapper.setProps({ isUser: true, taskListOverride });
wrapper.vm.taskList.forEach((el, i) => {
expect(el).to.eq(habits[i]);
});
});
});
describe('Methods', () => {
describe('Filter By Tags', () => {
beforeEach(() => {
tasks = [
{ tags: [3, 4] },
{ tags: [2, 3] },
{ tags: [] },
{ tags: [1, 3] },
];
});
it('returns all tasks if no tag is given', () => {
let returnedTasks = wrapper.vm.filterByTagList(tasks);
expect(returnedTasks).to.have.lengthOf(tasks.length);
tasks.forEach((task, i) => {
expect(returnedTasks[i]).to.eq(task);
});
});
it('returns tasks for given single tag', () => {
let returnedTasks = wrapper.vm.filterByTagList(tasks, [3]);
expect(returnedTasks).to.have.lengthOf(3);
expect(returnedTasks[0]).to.eq(tasks[0]);
expect(returnedTasks[1]).to.eq(tasks[1]);
expect(returnedTasks[2]).to.eq(tasks[3]);
});
it('returns tasks for given multiple tags', () => {
let returnedTasks = wrapper.vm.filterByTagList(tasks, [2, 3]);
expect(returnedTasks).to.have.lengthOf(1);
expect(returnedTasks[0]).to.eq(tasks[1]);
});
});
describe('Filter By Search Text', () => {
beforeEach(() => {
tasks = [
{
text: 'Hello world 1',
notes: '',
checklist: [],
},
{
text: 'Hello world 2',
notes: '',
checklist: [],
},
{
text: 'Generic Task Title',
notes: '',
checklist: [
{ text: 'Check 1' },
{ text: 'Check 2' },
{ text: 'Check 3' },
],
},
{
text: 'Hello world 3',
notes: 'Generic Task Note',
checklist: [
{ text: 'Checkitem 1' },
{ text: 'Checkitem 2' },
{ text: 'Checkitem 3' },
],
},
];
});
it('returns all tasks for empty search term', () => {
let returnedTasks = wrapper.vm.filterBySearchText(tasks);
expect(returnedTasks).to.have.lengthOf(tasks.length);
tasks.forEach((task, i) => {
expect(returnedTasks[i]).to.eq(task);
});
});
it('returns tasks for search term in title /i', () => {
['Title', 'TITLE', 'title', 'tItLe'].forEach((term) => {
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[2]);
});
});
it('returns tasks for search term in note /i', () => {
['Note', 'NOTE', 'note', 'nOtE'].forEach((term) => {
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
});
});
it('returns tasks for search term in checklist title /i', () => {
['Check', 'CHECK', 'check', 'cHeCK'].forEach((term) => {
let returnedTasks = wrapper.vm.filterBySearchText(tasks, term);
expect(returnedTasks[0]).to.eq(tasks[2]);
expect(returnedTasks[1]).to.eq(tasks[3]);
});
['Checkitem', 'CHECKITEM', 'checkitem', 'cHeCKiTEm'].forEach((term) => {
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
});
});
});
});
});

View File

@@ -0,0 +1,62 @@
import {
getTypeLabel,
getFilterLabels,
getActiveFilter,
} from 'client/libs/store/helpers/filterTasks.js';
describe('Filter Category for Tasks', () => {
describe('getTypeLabel', () => {
it('should return correct task type labels', () => {
expect(getTypeLabel('habit')).to.eq('habits');
expect(getTypeLabel('daily')).to.eq('dailies');
expect(getTypeLabel('todo')).to.eq('todos');
expect(getTypeLabel('reward')).to.eq('rewards');
});
});
describe('getFilterLabels', () => {
let habit, daily, todo, reward;
beforeEach(() => {
habit = ['all', 'yellowred', 'greenblue'];
daily = ['all', 'due', 'notDue'];
todo = ['remaining', 'scheduled', 'complete2'];
reward = ['all', 'custom', 'wishlist'];
});
it('should return all task type filter labels by type', () => {
// habits
getFilterLabels('habit').forEach((item, i) => {
expect(item).to.eq(habit[i]);
});
// dailys
getFilterLabels('daily').forEach((item, i) => {
expect(item).to.eq(daily[i]);
});
// todos
getFilterLabels('todo').forEach((item, i) => {
expect(item).to.eq(todo[i]);
});
// rewards
getFilterLabels('reward').forEach((item, i) => {
expect(item).to.eq(reward[i]);
});
});
});
describe('getActiveFilter', () => {
it('should return single function by default', () => {
let activeFilter = getActiveFilter('habit');
expect(activeFilter).to.be.an('object');
expect(activeFilter).to.have.all.keys('label', 'filterFn', 'default');
expect(activeFilter.default).to.be.true;
});
it('should return single function for given filter type', () => {
let activeFilterLabel = 'yellowred';
let activeFilter = getActiveFilter('habit', activeFilterLabel);
expect(activeFilter).to.be.an('object');
expect(activeFilter).to.have.all.keys('label', 'filterFn');
expect(activeFilter.label).to.eq(activeFilterLabel);
});
});
});

View File

@@ -0,0 +1,43 @@
import {
orderSingleTypeTasks,
// orderMultipleTypeTasks,
} from 'client/libs/store/helpers/orderTasks.js';
import shuffle from 'lodash/shuffle';
describe('Task Order Helper Function', () => {
let tasks, shuffledTasks, taskOrderList;
beforeEach(() => {
taskOrderList = [1, 2, 3, 4];
tasks = [];
taskOrderList.forEach(i => tasks.push({ _id: i, id: i }));
shuffledTasks = shuffle(tasks);
});
it('should return tasks as is for no task order', () => {
expect(orderSingleTypeTasks(shuffledTasks)).to.eq(shuffledTasks);
});
it('should return tasks in expected order', () => {
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
newOrderedTasks.forEach((item, index) => {
expect(item).to.eq(tasks[index]);
});
});
it('should return new tasks at end of expected order', () => {
let newTaskIds = [10, 15, 20];
newTaskIds.forEach(i => tasks.push({ _id: i, id: i }));
shuffledTasks = shuffle(tasks);
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
// checking tasks with order
newOrderedTasks.slice(0, taskOrderList.length).forEach((item, index) => {
expect(item).to.eq(tasks[index]);
});
// check for new task ids
newOrderedTasks.slice(-3).forEach(item => {
expect(item.id).to.be.oneOf(newTaskIds);
});
});
});

View File

@@ -0,0 +1,63 @@
import { hasClass } from 'client/store/getters/members';
describe('hasClass getter', () => {
it('returns false if level < 10', () => {
const member = {
stats: {
lvl: 5,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns false if member has disabled classes', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: true,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns false if member has not yet selected a class', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: false,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns true when all conditions are met', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(true);
});
});

View File

@@ -0,0 +1,118 @@
import generateStore from 'client/store';
describe('Store Getters for Tasks', () => {
let store, habits, dailys, todos, rewards;
beforeEach(() => {
store = generateStore();
// Get user preference data and user tasks order data
store.state.user.data = {
preferences: {},
tasksOrder: {
habits: [],
dailys: [],
todos: [],
rewards: [],
},
};
});
describe('Task List', () => {
beforeEach(() => {
habits = [
{ id: 1 },
{ id: 2 },
];
dailys = [
{ id: 3 },
{ id: 4 },
];
todos = [
{ id: 5 },
{ id: 6 },
];
rewards = [
{ id: 7 },
{ id: 8 },
];
store.state.tasks.data = {
habits,
dailys,
todos,
rewards,
};
});
it('should returns all tasks by task type', () => {
let returnedTasks = store.getters['tasks:getUnfilteredTaskList']('habit');
expect(returnedTasks).to.eq(habits);
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('daily');
expect(returnedTasks).to.eq(dailys);
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('todo');
expect(returnedTasks).to.eq(todos);
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('reward');
expect(returnedTasks).to.eq(rewards);
});
});
// @TODO add task filter check for rewards and dailys
describe('Task Filters', () => {
beforeEach(() => {
habits = [
// weak habit
{ value: 0 },
// strong habit
{ value: 2 },
];
todos = [
// scheduled todos
{ completed: false, date: 'Mon, 15 Jan 2018 12:18:29 GMT' },
// completed todos
{ completed: true },
];
store.state.tasks.data = {
habits,
todos,
};
});
it('should return weak habits', () => {
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
type: 'habit',
filterType: 'yellowred',
});
expect(returnedTasks[0]).to.eq(habits[0]);
});
it('should return strong habits', () => {
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
type: 'habit',
filterType: 'greenblue',
});
expect(returnedTasks[0]).to.eq(habits[1]);
});
it('should return scheduled todos', () => {
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
type: 'todo',
filterType: 'scheduled',
});
expect(returnedTasks[0]).to.eq(todos[0]);
});
it('should return completed todos', () => {
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
type: 'todo',
filterType: 'complete2',
});
expect(returnedTasks[0]).to.eq(todos[1]);
});
});
});

View File

@@ -0,0 +1,84 @@
import {
generateUser,
} from '../../helpers/common.helper';
import getOfficialPinnedItems from '../../../website/common/script/libs/getOfficialPinnedItems.js';
import inAppRewards from '../../../website/common/script/libs/inAppRewards';
describe('inAppRewards', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
let testPinnedItems;
let testPinnedItemsOrder;
beforeEach(() => {
user = generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
});
it('returns the pinned items in the correct order', () => {
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
let result = inAppRewards(user);
expect(result[2].path).to.eql('armoire');
expect(result[9].path).to.eql('potion');
});
it('does not return seasonal items which have been unpinned', () => {
if (officialPinnedItems.length === 0) {
return; // if no seasonal items, this test is not applicable
}
let testUnpinnedItem = officialPinnedItems[0];
let testUnpinnedPath = testUnpinnedItem.path;
let testUnpinnedItems = [
{ type: testUnpinnedItem.type, path: testUnpinnedPath},
];
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
user.unpinnedItems = testUnpinnedItems;
let result = inAppRewards(user);
let itemPaths = result.map(item => item.path);
expect(itemPaths).to.not.include(testUnpinnedPath);
});
});

View File

@@ -4,7 +4,7 @@ import {
generateUser, generateUser,
} from '../../../helpers/common.helper'; } from '../../../helpers/common.helper';
import count from '../../../../website/common/script/count'; import count from '../../../../website/common/script/count';
import buyArmoire from '../../../../website/common/script/ops/buy/buyArmoire'; import {BuyArmoireOperation} from '../../../../website/common/script/ops/buy/buyArmoire';
import randomVal from '../../../../website/common/script/libs/randomVal'; import randomVal from '../../../../website/common/script/libs/randomVal';
import content from '../../../../website/common/script/content/index'; import content from '../../../../website/common/script/content/index';
import { import {
@@ -33,6 +33,12 @@ describe('shared.ops.buyArmoire', () => {
let YIELD_EXP = 0.9; let YIELD_EXP = 0.9;
let analytics = {track () {}}; let analytics = {track () {}};
function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => { beforeEach(() => {
user = generateUser({ user = generateUser({
stats: { gp: 200 }, stats: { gp: 200 },

View File

@@ -2,7 +2,7 @@
import { import {
generateUser, generateUser,
} from '../../../helpers/common.helper'; } from '../../../helpers/common.helper';
import buyHealthPotion from '../../../../website/common/script/ops/buy/buyHealthPotion'; import { BuyHealthPotionOperation } from '../../../../website/common/script/ops/buy/buyHealthPotion';
import { import {
NotAuthorized, NotAuthorized,
} from '../../../../website/common/script/libs/errors'; } from '../../../../website/common/script/libs/errors';
@@ -12,6 +12,12 @@ describe('shared.ops.buyHealthPotion', () => {
let user; let user;
let analytics = {track () {}}; let analytics = {track () {}};
function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => { beforeEach(() => {
user = generateUser({ user = generateUser({
items: { items: {

View File

@@ -4,14 +4,20 @@ import sinon from 'sinon'; // eslint-disable-line no-shadow
import { import {
generateUser, generateUser,
} from '../../../helpers/common.helper'; } from '../../../helpers/common.helper';
import buyGear from '../../../../website/common/script/ops/buy/buyGear'; import {BuyMarketGearOperation} from '../../../../website/common/script/ops/buy/buyMarketGear';
import shared from '../../../../website/common/script'; import shared from '../../../../website/common/script';
import { import {
BadRequest, NotAuthorized, NotFound, BadRequest, NotAuthorized, NotFound,
} from '../../../../website/common/script/libs/errors'; } from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n'; import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyGear', () => { function buyGear (user, req, analytics) {
let buyOp = new BuyMarketGearOperation(user, req, analytics);
return buyOp.purchase();
}
describe('shared.ops.buyMarketGear', () => {
let user; let user;
let analytics = {track () {}}; let analytics = {track () {}};
@@ -111,6 +117,31 @@ describe('shared.ops.buyGear', () => {
} }
}); });
it('does not buy equipment of different class', (done) => {
user.stats.gp = 82;
user.stats.class = 'warrior';
try {
buyGear(user, {params: {key: 'weapon_special_winter2018Rogue'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
done();
}
});
it('does not buy equipment in bulk', (done) => {
user.stats.gp = 82;
try {
buyGear(user, {params: {key: 'armor_warrior_1'}, quantity: 3});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAbleToBuyInBulk'));
done();
}
});
// TODO after user.ops.equip is done // TODO after user.ops.equip is done
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => { xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
user.stats.gp = 100; user.stats.gp = 100;

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../helpers/common.helper'; } from '../../../helpers/common.helper';
import buyQuest from '../../../../website/common/script/ops/buy/buyQuest'; import {BuyQuestWithGoldOperation} from '../../../../website/common/script/ops/buy/buyQuest';
import { import {
BadRequest, BadRequest,
NotAuthorized, NotAuthorized,
@@ -13,6 +13,12 @@ describe('shared.ops.buyQuest', () => {
let user; let user;
let analytics = {track () {}}; let analytics = {track () {}};
function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => { beforeEach(() => {
user = generateUser(); user = generateUser();
sinon.stub(analytics, 'track'); sinon.stub(analytics, 'track');
@@ -36,6 +42,43 @@ describe('shared.ops.buyQuest', () => {
expect(analytics.track).to.be.calledOnce; expect(analytics.track).to.be.calledOnce;
}); });
it('buys a Quest scroll with the right quantity if a string is passed for quantity', () => {
user.stats.gp = 1000;
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: '3',
}, analytics);
expect(user.items.quests).to.eql({
dilatoryDistress1: 4,
});
});
it('does not buy a Quest scroll when an invalid quantity is passed', (done) => {
user.stats.gp = 1000;
try {
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: 'a',
}, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
expect(user.items.quests).to.eql({});
expect(user.stats.gp).to.equal(1000);
done();
}
});
it('does not buy Quests without enough Gold', (done) => { it('does not buy Quests without enough Gold', (done) => {
user.stats.gp = 1; user.stats.gp = 1;
try { try {

View File

@@ -1,4 +1,5 @@
import purchase from '../../../../website/common/script/ops/buy/purchase'; import purchase from '../../../../website/common/script/ops/buy/purchase';
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits'; import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
import { import {
BadRequest, BadRequest,
@@ -25,10 +26,12 @@ describe('shared.ops.purchase', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(analytics, 'track'); sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
}); });
afterEach(() => { afterEach(() => {
analytics.track.restore(); analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
}); });
context('failure conditions', () => { context('failure conditions', () => {
@@ -87,6 +90,19 @@ describe('shared.ops.purchase', () => {
} }
}); });
it('prevents user from buying an invalid quantity', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
purchase(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
it('returns error when unknown type is provided', (done) => { it('returns error when unknown type is provided', (done) => {
try { try {
purchase(user, {params: {type: 'randomType', key: 'gem'}}); purchase(user, {params: {type: 'randomType', key: 'gem'}});
@@ -161,6 +177,12 @@ describe('shared.ops.purchase', () => {
user.stats.gp = goldPoints; user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0; user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id'; user.purchased.plan.customerId = 'customer-id';
user.pinnedItems.push({type: 'eggs', key: 'Wolf'});
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
user.pinnedItems.push({type: 'gear', key: 'headAccessory_special_tigerEars'});
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
}); });
it('purchases gems', () => { it('purchases gems', () => {
@@ -189,6 +211,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}, analytics); purchase(user, {params: {type, key}}, analytics);
expect(user.items[type][key]).to.equal(1); expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce; expect(analytics.track).to.be.calledOnce;
}); });
@@ -199,6 +222,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}); purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1); expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
}); });
it('purchases food', () => { it('purchases food', () => {
@@ -208,6 +232,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}); purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1); expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
}); });
it('purchases quests', () => { it('purchases quests', () => {
@@ -217,6 +242,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}); purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1); expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
}); });
it('purchases gear', () => { it('purchases gear', () => {
@@ -226,6 +252,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}); purchase(user, {params: {type, key}});
expect(user.items.gear.owned[key]).to.be.true; expect(user.items.gear.owned[key]).to.be.true;
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
}); });
it('purchases quest bundles', () => { it('purchases quest bundles', () => {
@@ -248,6 +275,7 @@ describe('shared.ops.purchase', () => {
expect(user.balance).to.equal(startingBalance - price); expect(user.balance).to.equal(startingBalance - price);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
clock.restore(); clock.restore();
}); });
}); });

View File

@@ -26,10 +26,11 @@ describe('shared.ops.releaseBoth', () => {
user.items.currentMount = animal; user.items.currentMount = animal;
user.items.currentPet = animal; user.items.currentPet = animal;
user.balance = 1.5;
user.achievements.triadBingo = true;
}); });
it('returns an error when user balance is too low and user does not have triadBingo', (done) => { xit('returns an error when user balance is too low and user does not have triadBingo', (done) => {
user.balance = 0; user.balance = 0;
try { try {

View File

@@ -55,10 +55,15 @@ export function generateReq (options = {}) {
body: {}, body: {},
query: {}, query: {},
headers: {}, headers: {},
header: sandbox.stub().returns(null), header (header) {
return this.headers[header];
},
session: {},
}; };
return defaultsDeep(options, defaultReq); const req = defaultsDeep(options, defaultReq);
return req;
} }
export function generateNext (func) { export function generateNext (func) {

View File

@@ -9,15 +9,23 @@ div
h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}} h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}}
p {{currentTip}} p {{currentTip}}
#app(:class='{"casting-spell": castingSpell}') #app(:class='{"casting-spell": castingSpell}')
amazon-payments-modal banned-account-modal
amazon-payments-modal(v-if='!isStaticPage')
snackbars snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage") router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else) template(v-else)
template(v-if="isUserLoaded") template(v-if="isUserLoaded")
div.resting-banner(v-if="showRestingBanner")
span.content
span.label {{ $t('innCheckOutBanner') }}
span.separator |
span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }}
div.closepadding(@click="hideBanner()")
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
notifications-display notifications-display
app-menu app-menu(:class='{"restingInn": showRestingBanner}')
.container-fluid .container-fluid
app-header app-header(:class='{"restingInn": showRestingBanner}')
buyModal( buyModal(
:item="selectedItemToBuy || {}", :item="selectedItemToBuy || {}",
:withPin="true", :withPin="true",
@@ -34,13 +42,15 @@ div
div(:class='{sticky: user.preferences.stickyHeader}') div(:class='{sticky: user.preferences.stickyHeader}')
router-view router-view
app-footer app-footer
audio#sound(autoplay, ref="sound") audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource") source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source") source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
#loading-screen-inapp { #loading-screen-inapp {
#melior { #melior {
margin: 0 auto; margin: 0 auto;
@@ -53,7 +63,7 @@ div
} }
h2 { h2 {
color: #fff; color: $white;
font-size: 32px; font-size: 32px;
font-weight: bold; font-weight: bold;
} }
@@ -72,10 +82,10 @@ div
.notification { .notification {
border-radius: 1000px; border-radius: 1000px;
background-color: #24cc8f; background-color: $green-10;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12); box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: .5em 1em; padding: .5em 1em;
color: #fff; color: $white;
margin-top: .5em; margin-top: .5em;
margin-bottom: .5em; margin-bottom: .5em;
} }
@@ -93,21 +103,76 @@ div
} }
</style> </style>
<style> <style lang='scss'>
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */ /* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal, .modal-open { .modal, .modal-open {
overflow-y: scroll !important; overflow-y: scroll !important;
} }
.modal-backdrop.show { .modal-backdrop.show {
opacity: 1 !important; opacity: .9 !important;
background-color: rgba(67, 40, 116, 0.9) !important; background-color: $purple-100 !important;
} }
/* Push progress bar above modals */ /* Push progress bar above modals */
#nprogress .bar { #nprogress .bar {
z-index: 1043 !important; /* Must stay above nav bar */ z-index: 1043 !important; /* Must stay above nav bar */
} }
.restingInn {
.navbar {
top: 40px;
}
#app-header {
margin-top: 96px !important;
}
}
.resting-banner {
width: 100%;
height: 40px;
background-color: $blue-10;
position: fixed;
top: 0;
z-index: 1030;
display: flex;
.content {
height: 24px;
line-height: 1.71;
text-align: center;
color: $white;
margin: auto;
}
.closepadding {
margin: 11px 24px;
display: inline-block;
position: absolute;
right: 0;
top: 0;
cursor: pointer;
span svg path {
stroke: $blue-500;
}
}
.separator {
color: $blue-100;
margin: 0px 15px;
}
.resume {
font-weight: bold;
cursor: pointer;
}
}
</style> </style>
<script> <script>
@@ -128,6 +193,11 @@ import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal'; import amazonPaymentsModal from 'client/components/payments/amazonModal';
import spellsMixin from 'client/mixins/spells'; import spellsMixin from 'client/mixins/spells';
import svgClose from 'assets/svg/close.svg';
import bannedAccountModal from 'client/components/bannedAccountModal';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS.COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default { export default {
mixins: [notifications, spellsMixin], mixins: [notifications, spellsMixin],
name: 'app', name: 'app',
@@ -140,9 +210,13 @@ export default {
BuyModal, BuyModal,
SelectMembersModal, SelectMembersModal,
amazonPaymentsModal, amazonPaymentsModal,
bannedAccountModal,
}, },
data () { data () {
return { return {
icons: Object.freeze({
close: svgClose,
}),
selectedItemToBuy: null, selectedItemToBuy: null,
selectedSpellToBuy: null, selectedSpellToBuy: null,
@@ -152,6 +226,7 @@ export default {
}, },
loading: true, loading: true,
currentTipNumber: 0, currentTipNumber: 0,
bannerHidden: false,
}; };
}, },
computed: { computed: {
@@ -172,13 +247,17 @@ export default {
return this.$t(`tip${tipNumber}`); return this.$t(`tip${tipNumber}`);
}, },
showRestingBanner () {
return !this.bannerHidden && this.user.preferences.sleep;
},
}, },
created () { created () {
this.$root.$on('playSound', (sound) => { this.$root.$on('playSound', (sound) => {
let theme = this.user.preferences.sound; let theme = this.user.preferences.sound;
if (!theme || theme === 'off') if (!theme || theme === 'off') {
return; return;
}
let file = `/static/audio/${theme}/${sound}`; let file = `/static/audio/${theme}/${sound}`;
this.sound = { this.sound = {
@@ -214,6 +293,8 @@ export default {
return response; return response;
}, (error) => { }, (error) => {
if (error.response.status >= 400) { if (error.response.status >= 400) {
this.checkForBannedUser(error);
// Check for conditions to reset the user auth // Check for conditions to reset the user auth
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(error.response.data) !== -1) { if (invalidUserMessage.indexOf(error.response.data) !== -1) {
@@ -292,6 +373,11 @@ export default {
document.title = title; document.title = title;
}); });
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) { if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks // Load the user and the user tasks
Promise.all([ Promise.all([
@@ -314,7 +400,6 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
// Load external scripts after the app has been rendered // Load external scripts after the app has been rendered
setupPayments(); setupPayments();
Analytics.load();
}); });
}).catch((err) => { }).catch((err) => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
@@ -338,6 +423,25 @@ export default {
if (loadingScreen) document.body.removeChild(loadingScreen); if (loadingScreen) document.body.removeChild(loadingScreen);
}, },
methods: { methods: {
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const errorMessage = error.response.data.message;
// Case where user is not logged in
if (!parseSettings) {
return;
}
const bannedMessage = this.$t('accountSuspended', {
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: parseSettings.auth.apiId,
});
if (errorMessage !== bannedMessage) return;
this.$root.$emit('bv::show::modal', 'banned-account');
},
initializeModalStack () { initializeModalStack () {
// Manage modals // Manage modals
this.$root.$on('bv::show::modal', (modalId, data = {}) => { this.$root.$on('bv::show::modal', (modalId, data = {}) => {
@@ -426,7 +530,7 @@ export default {
if (!item) if (!item)
return false; return false;
if (item.purchaseType === 'card') if (['card', 'debuffPotion'].includes(item.purchaseType))
return false; return false;
return true; return true;
@@ -447,6 +551,10 @@ export default {
this.$root.$emit('bv::show::modal', 'select-member-modal'); this.$root.$emit('bv::show::modal', 'select-member-modal');
} }
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
}, },
async memberSelected (member) { async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member); await this.castStart(this.selectedSpellToBuy, member);
@@ -462,6 +570,12 @@ export default {
hideLoadingScreen () { hideLoadingScreen () {
this.loading = false; this.loading = false;
}, },
hideBanner () {
this.bannerHidden = true;
},
resumeDamage () {
this.$store.dispatch('user:sleep');
},
}, },
}; };
</script> </script>

View File

@@ -1,84 +1,66 @@
.promo_armoire_background_201803 { .promo_armoire_background_201804 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -735px; background-position: -142px -587px;
width: 141px; width: 141px;
height: 441px; height: 441px;
} }
.promo_cupid_potions { .promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -284px -735px; background-position: -532px 0px;
width: 138px; width: 325px;
height: 441px; height: 336px;
} }
.promo_dysheartener { .promo_mystery_201803 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px 0px; background-position: -695px -337px;
width: 730px; width: 114px;
height: 170px; height: 90px;
}
.promo_hippogriff {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1172px -587px;
width: 105px;
height: 105px;
}
.promo_hugabug_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1172px 0px;
width: 141px;
height: 441px;
}
.promo_mystery_201802 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -327px;
width: 372px;
height: 196px;
} }
.promo_rainbow_potions { .promo_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -735px; background-position: -284px -587px;
width: 141px; width: 141px;
height: 441px; height: 441px;
} }
.promo_seasonalshop_broken { .promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -751px -171px; background-position: -532px -337px;
width: 198px; width: 162px;
height: 138px;
}
.promo_shimmer_pastel {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -735px;
width: 354px;
height: 147px; height: 147px;
} }
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -587px;
width: 360px;
height: 147px;
}
.promo_spring_fling_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -587px;
width: 141px;
height: 588px;
}
.promo_take_this { .promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -950px -171px; background-position: -532px -476px;
width: 114px; width: 114px;
height: 87px; height: 87px;
} }
.promo_valentines { .scene_positivity {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px -171px;
width: 309px;
height: 147px;
}
.scene_achievement {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -524px;
width: 339px;
height: 210px;
}
.scene_coding {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -373px -327px;
width: 150px;
height: 150px;
}
.scene_sweeping {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1172px -442px;
width: 138px;
height: 144px;
}
.scene_tavern {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px; background-position: 0px 0px;
width: 440px; width: 531px;
height: 326px; height: 243px;
}
.scene_video_games {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -244px;
width: 339px;
height: 342px;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,72 @@
.quest_TEMPLATE_FOR_MISSING_IMAGE { .phobia_dysheartener {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -251px -1519px; background-position: 0px -1510px;
width: 221px; width: 201px;
height: 39px; height: 195px;
}
.quest_armadillo {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -440px;
width: 219px;
height: 219px;
}
.quest_atom1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1376px -1332px;
width: 250px;
height: 150px;
}
.quest_atom2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -633px -1510px;
width: 207px;
height: 138px;
}
.quest_atom3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -202px -1510px;
width: 216px;
height: 180px;
}
.quest_axolotl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -232px;
width: 219px;
height: 219px;
}
.quest_badger {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -232px;
width: 219px;
height: 219px;
}
.quest_basilist {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -191px -1706px;
width: 189px;
height: 141px;
}
.quest_beetle {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -1079px;
width: 204px;
height: 201px;
}
.quest_bunny {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1322px -1112px;
width: 210px;
height: 186px;
}
.quest_butterfly {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -452px;
width: 219px;
height: 219px;
} }
.quest_cheetah { .quest_cheetah {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -672px; background-position: -440px -452px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
@@ -18,91 +78,91 @@
} }
.quest_dilatory { .quest_dilatory {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -232px; background-position: -880px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dilatoryDistress1 { .quest_dilatoryDistress1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -1085px; background-position: -1540px -868px;
width: 210px; width: 210px;
height: 210px; height: 210px;
} }
.quest_dilatoryDistress2 { .quest_dilatoryDistress2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -1023px; background-position: -1757px -573px;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.quest_dilatoryDistress3 { .quest_dilatoryDistress3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px 0px; background-position: -220px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dilatory_derby { .quest_dilatory_derby {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px 0px; background-position: -880px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dustbunnies { .quest_dustbunnies {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -220px; background-position: -440px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_egg { .quest_egg {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -362px; background-position: -1757px -214px;
width: 165px; width: 165px;
height: 207px; height: 207px;
} }
.quest_evilsanta { .quest_evilsanta {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -1174px; background-position: -1757px -875px;
width: 118px; width: 118px;
height: 131px; height: 131px;
} }
.quest_evilsanta2 { .quest_evilsanta2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -452px; background-position: -1100px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_falcon { .quest_falcon {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -452px; background-position: -1100px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_ferret { .quest_ferret {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px 0px; background-position: -1100px -440px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_frog { .quest_frog {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -1112px; background-position: -660px -1112px;
width: 221px; width: 221px;
height: 213px; height: 213px;
} }
.quest_ghost_stag { .quest_ghost_stag {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -440px; background-position: -220px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_goldenknight1 { .quest_goldenknight1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -672px; background-position: -220px -892px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_goldenknight2 { .quest_goldenknight2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1311px -1332px; background-position: -874px -1332px;
width: 250px; width: 250px;
height: 150px; height: 150px;
} }
@@ -114,115 +174,115 @@
} }
.quest_gryphon { .quest_gryphon {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -1332px; background-position: -657px -1332px;
width: 216px; width: 216px;
height: 177px; height: 177px;
} }
.quest_guineapig { .quest_guineapig {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -672px; background-position: -1100px -892px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_harpy { .quest_harpy {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px 0px; background-position: -1320px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_hedgehog { .quest_hedgehog {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1332px; background-position: -1102px -1112px;
width: 219px; width: 219px;
height: 186px; height: 186px;
} }
.quest_hippo { .quest_hippo {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -440px; background-position: -660px -892px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_horse { .quest_horse {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -660px; background-position: -1320px -660px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_kraken { .quest_kraken {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -877px -1332px; background-position: -223px -1332px;
width: 216px; width: 216px;
height: 177px; height: 177px;
} }
.quest_lostMasterclasser1 { .quest_lostMasterclasser1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -892px; background-position: 0px -1112px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_lostMasterclasser2 { .quest_lostMasterclasser2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -892px; background-position: -220px -1112px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_lostMasterclasser3 { .quest_lostMasterclasser3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -892px; background-position: -1320px -880px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_mayhemMistiflying1 { .quest_mayhemMistiflying1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -570px; background-position: -1757px -422px;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.quest_mayhemMistiflying2 { .quest_mayhemMistiflying2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -892px; background-position: -1320px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_mayhemMistiflying3 { .quest_mayhemMistiflying3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px 0px; background-position: -440px -892px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_monkey { .quest_monkey {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -220px; background-position: -1100px -660px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moon1 { .quest_moon1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -651px; background-position: -1540px -434px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_moon2 { .quest_moon2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px 0px; background-position: 0px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moon3 { .quest_moon3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -880px; background-position: -880px -440px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moonstone1 { .quest_moonstone1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1112px; background-position: -660px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moonstone2 { .quest_moonstone2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -1112px; background-position: -440px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
@@ -234,19 +294,19 @@
} }
.quest_nudibranch { .quest_nudibranch {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -434px; background-position: -1540px -651px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_octopus { .quest_octopus {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -1332px; background-position: 0px -1332px;
width: 222px; width: 222px;
height: 177px; height: 177px;
} }
.quest_owl { .quest_owl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -660px; background-position: -880px -892px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
@@ -258,145 +318,85 @@
} }
.quest_penguin { .quest_penguin {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -178px; background-position: 0px -1706px;
width: 190px; width: 190px;
height: 183px; height: 183px;
} }
.quest_pterodactyl { .quest_pterodactyl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -440px; background-position: -880px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_rat { .quest_rat {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -892px; background-position: -660px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_rock { .quest_rock {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -868px; background-position: -1540px 0px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_rooster { .quest_rooster {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1670px; background-position: -419px -1510px;
width: 213px; width: 213px;
height: 174px; height: 174px;
} }
.quest_sabretooth { .quest_sabretooth {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -892px; background-position: -660px -452px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_sheep { .quest_sheep {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -220px; background-position: 0px -452px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_slime { .quest_slime {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -672px; background-position: -660px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_sloth { .quest_sloth {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -672px; background-position: 0px -232px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_snail { .quest_snail {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1102px -1112px; background-position: -882px -1112px;
width: 219px; width: 219px;
height: 213px; height: 213px;
} }
.quest_snake { .quest_snake {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1322px -1112px; background-position: -440px -1332px;
width: 216px; width: 216px;
height: 177px; height: 177px;
} }
.quest_spider { .quest_spider {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1519px; background-position: -1125px -1332px;
width: 250px; width: 250px;
height: 150px; height: 150px;
} }
.quest_squirrel {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -892px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity1 { .quest_stoikalmCalamity1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png'); background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -721px; background-position: -1757px -724px;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.quest_stoikalmCalamity2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -220px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -452px;
width: 219px;
height: 219px;
}
.quest_taskwoodsTerror1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px -872px;
width: 150px;
height: 150px;
}
.quest_taskwoodsTerror2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px 0px;
width: 216px;
height: 216px;
}
.quest_taskwoodsTerror3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -452px;
width: 219px;
height: 219px;
}
.quest_treeling {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -443px -1332px;
width: 216px;
height: 177px;
}
.quest_trex {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1932px 0px;
width: 204px;
height: 177px;
}
.quest_trex_undead {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1094px -1332px;
width: 216px;
height: 177px;
}
.quest_triceratops {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -232px;
width: 219px;
height: 219px;
}
.quest_turtle {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -232px;
width: 219px;
height: 219px;
}
.quest_unicorn {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -1112px;
width: 219px;
height: 219px;
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More