Merge branch 'develop' into negue/modal-notifications

This commit is contained in:
Matteo Pagliazzi
2018-10-13 21:18:16 +02:00
882 changed files with 46962 additions and 35748 deletions
+2 -1
View File
@@ -17,7 +17,7 @@
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
"NODE_ENV":"development",
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
"CRON_SAFE_MODE":"false",
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",
@@ -39,6 +39,7 @@
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
"AMAZON_PAYMENTS": {
"SELLER_ID": "SELLER_ID",
"CLIENT_ID": "CLIENT_ID",
@@ -0,0 +1,48 @@
import monk from 'monk';
import nconf from 'nconf';
/*
* Output data on users who completed all the To-Do tasks in the 2018 Back-to-School Challenge.
* User ID,Profile Name
*/
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
const CHALLENGE_ID = '0acb1d56-1660-41a4-af80-9259f080b62b';
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
let dbTasks = monk(CONNECTION_STRING).get('tasks', { castIds: false });
function usersReport() {
console.info('User ID,Profile Name');
let userCount = 0;
dbUsers.find(
{challenges: CHALLENGE_ID},
{fields:
{_id: 1, 'profile.name': 1}
},
).each((user, {close, pause, resume}) => {
pause();
userCount++;
let completedTodos = 0;
return dbTasks.find(
{
userId: user._id,
'challenge.id': CHALLENGE_ID,
type: 'todo',
},
{fields: {completed: 1}}
).each((task) => {
if (task.completed) completedTodos++;
}).then(() => {
if (completedTodos >= 7) {
console.info(`${user._id},${user.profile.name}`);
}
resume();
});
}).then(() => {
console.info(`${userCount} users reviewed`);
return process.exit(0);
});
}
module.exports = usersReport;
@@ -0,0 +1,147 @@
const migrationName = '20180811_inboxOutsideUser.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Move inbox messages from the user model to their own collection
*/
const monk = require('monk');
const nconf = require('nconf');
const uuid = require('uuid').v4;
const Inbox = require('../website/server/models/message').inboxModel;
const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
const dbInboxes = monk(connectionString).get('inboxes', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 1000,
fields: ['_id', 'inbox'],
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
let msgCount = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return;
}
let usersPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => {
return processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (msgCount % progressCount === 0) console.warn(`${msgCount } messages processed`);
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
const oldInboxMessages = user.inbox.messages || {};
const oldInboxMessagesIds = Object.keys(oldInboxMessages);
msgCount += oldInboxMessagesIds.length;
const newInboxMessages = oldInboxMessagesIds.map(msgId => {
const msg = oldInboxMessages[msgId];
if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens
console.log('missing message or message _id and id', msg);
throw new Error('error!');
}
if (msg.id && !msg._id) msg._id = msg.id;
if (msg._id && !msg.id) msg.id = msg._id;
const newMsg = new Inbox(msg);
newMsg.ownerId = user._id;
return newMsg.toJSON();
});
const promises = newInboxMessages.map(newMsg => {
return (async function fn () {
const existing = await dbInboxes.find({_id: newMsg._id});
if (existing.length > 0) {
if (
existing[0].ownerId === newMsg.ownerId &&
existing[0].text === newMsg.text &&
existing[0].uuid === newMsg.uuid &&
existing[0].sent === newMsg.sent
) {
return null;
}
newMsg.id = newMsg._id = uuid();
}
return newMsg;
})();
});
return Promise.all(promises)
.then((filteredNewMsg) => {
filteredNewMsg = filteredNewMsg.filter(m => Boolean(m && m.id && m._id && m.id == m._id));
return dbInboxes.insert(filteredNewMsg);
}).then(() => {
return dbUsers.update({_id: user._id}, {
$set: {
migration: migrationName,
'inbox.messages': {},
},
});
}).catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
console.warn(`\n${ msgCount } messages 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;
+9 -2
View File
@@ -17,5 +17,12 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./users/takeThis.js');
processUsers();
const processUsers = require('../scripts/gdpr-delete-users.js');
processUsers()
.then(function success () {
process.exit(0);
})
.catch(function failure (err) {
console.log(err);
process.exit(1);
});
+107
View File
@@ -0,0 +1,107 @@
const MIGRATION_NAME = '20181003_username_email.js';
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Send emails to eligible users announcing upcoming username changes
*/
import monk from 'monk';
import nconf from 'nconf';
import { sendTxn } from '../../website/server/libs/email';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 100,
fields: [
'_id',
'auth',
'preferences',
'profile',
], // 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(() => delay(7000))
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
dbUsers.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}});
sendTxn(
user,
'username-change',
[{name: 'UNSUB_EMAIL_TYPE_URL', content: '/user/settings/notifications?unsubFrom=importantAnnouncements'},
{name: 'LOGIN_NAME', content: user.auth.local.username}]
);
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 delay (t, v) {
return new Promise(function batchPause (resolve) {
setTimeout(resolve.bind(null, v), t);
});
}
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;
+99
View File
@@ -0,0 +1,99 @@
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Generate usernames for users who lack them
*/
import monk from 'monk';
import nconf from 'nconf';
import { generateUsername } from '../../website/server/libs/auth/utils';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'auth.local.username': {$exists: false},
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, // Initial coverage for users active within last 6 months
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'auth',
], // 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++;
if (!user.auth.local.username) {
const newName = generateUsername();
dbUsers.update(
{_id: user._id},
{$set:
{
'auth.local.username': newName,
'auth.local.lowerCaseUsername': newName,
},
}
);
}
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;
+2 -2
View File
@@ -1,14 +1,14 @@
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201807.js'; // Update per month
const migrationName = 'mystery-items-201808.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201807', 'head_mystery_201807'];
const MYSTERY_ITEMS = ['armor_mystery_201809', 'head_mystery_201809'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
+2 -2
View File
@@ -1,4 +1,4 @@
let migrationName = '20180801_takeThis.js'; // Update per month
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
@@ -15,7 +15,7 @@ function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['081f8912-3526-47d5-984f-f71bbeec77fc']}, // Update per month
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {
+2583 -1723
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.57.1",
"version": "4.65.3",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,9 +9,9 @@
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"apn": "^2.2.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.329.0",
"axios": "^0.18.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
@@ -26,7 +26,7 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^3.0.0",
"bcrypt": "^3.0.1",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
@@ -53,7 +53,7 @@
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.9.4",
"in-app-purchase": "^1.10.2",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
@@ -62,7 +62,7 @@
"method-override": "^2.3.5",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.1.2",
"mongoose": "^5.1.3",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
@@ -83,6 +83,8 @@
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.2",
"short-uuid": "^3.0.0",
"smartbanner.js": "^1.9.1",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",
+88
View File
@@ -0,0 +1,88 @@
/* eslint-disable no-console */
import axios from 'axios';
import { model as User } from '../website/server/models/user';
import nconf from 'nconf';
const AMPLITUDE_KEY = nconf.get('AMPLITUDE_KEY');
const AMPLITUDE_SECRET = nconf.get('AMPLITUDE_SECRET');
const BASE_URL = nconf.get('BASE_URL');
async function _deleteAmplitudeData (userId, email) {
const response = await axios.post(
'https://amplitude.com/api/2/deletions/users',
{
user_ids: userId, // eslint-disable-line camelcase
requester: email,
},
{
auth: {
username: AMPLITUDE_KEY,
password: AMPLITUDE_SECRET,
},
}
).catch((err) => {
console.log(err.response.data);
});
if (response) console.log(`${response.status} ${response.statusText}`);
}
async function _deleteHabiticaData (user) {
await User.update(
{_id: user._id},
{$set: {
'auth.local.passwordHashMethod': 'bcrypt',
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
}}
);
const response = await axios.delete(
`${BASE_URL}/api/v3/user`,
{
data: {
password: 'test',
},
headers: {
'x-api-user': user._id,
'x-api-key': user.apiToken,
},
}
).catch((err) => {
console.log(err.response.data);
});
if (response) {
console.log(`${response.status} ${response.statusText}`);
if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`);
}
}
async function _processEmailAddress (email) {
const emailRegex = new RegExp(`^${email}`, 'i');
const users = await User.find({
$or: [
{'auth.local.email': emailRegex},
{'auth.facebook.emails.value': emailRegex},
{'auth.google.emails.value': emailRegex},
]},
{
_id: 1,
apiToken: 1,
auth: 1,
}).exec();
if (users.length < 1) {
console.log(`No users found with email address ${email}`);
} else {
for (const user of users) {
await _deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
await _deleteHabiticaData(user); // eslint-disable-line no-await-in-loop
}
}
}
function deleteUserData (emails) {
const emailPromises = emails.map(_processEmailAddress);
return Promise.all(emailPromises);
}
module.exports = deleteUserData;
-21
View File
@@ -1589,27 +1589,6 @@ describe('cron', () => {
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
describe('login incentives', () => {
+19
View File
@@ -107,6 +107,25 @@ describe('Password Utilities', () => {
}
});
it('defaults to SHA1 encryption if salt is provided', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: '',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({
+8
View File
@@ -178,4 +178,12 @@ describe('taskManager', () => {
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
it('moves tasks to a specified position out of length', async () => {
let order = ['task-id-1'];
moveTask(order, 'task-id-2', 2);
expect(order).to.eql(['task-id-1', 'task-id-2']);
});
});
+110 -42
View File
@@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
@@ -48,6 +48,11 @@ describe('Group Model', () => {
party: { _id: party._id },
profile: { name: 'Participating Member' },
});
sleepingParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true },
});
nonParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Non-Participating Member' },
@@ -61,6 +66,7 @@ describe('Group Model', () => {
party.save(),
questLeader.save(),
participatingMember.save(),
sleepingParticipatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
@@ -80,6 +86,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -175,6 +182,34 @@ describe('Group Model', () => {
expect(party._processBossQuest).to.not.be.called;
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
});
it('does not call _processBossQuest when user is resting in the inn', async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
party.quest.key = 'evilsanta2';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
});
context('Boss Quests', () => {
@@ -216,17 +251,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -236,6 +274,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -248,17 +287,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -497,9 +539,11 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
@@ -508,6 +552,9 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
});
});
});
@@ -647,6 +694,7 @@ describe('Group Model', () => {
it('returns an array of members whose quest status set to true', () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
@@ -654,6 +702,7 @@ describe('Group Model', () => {
expect(party.getParticipatingQuestMembers()).to.eql([
participatingMember._id,
sleepingParticipatingMember._id,
questLeader._id,
]);
});
@@ -756,11 +805,12 @@ describe('Group Model', () => {
it('removes user from group quest', async () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
party.memberCount = 4;
party.memberCount = 5;
await party.save();
await party.leave(participatingMember);
@@ -768,6 +818,7 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(party.quest.members).to.eql({
[questLeader._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
});
@@ -775,6 +826,7 @@ describe('Group Model', () => {
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -846,6 +898,7 @@ describe('Group Model', () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -967,32 +1020,6 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('cuts down chat to 200 messages', () => {
for (let i = 0; i < 220; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(220);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(200);
});
it('cuts down chat to 400 messages when group is subcribed', () => {
party.purchased.plan.customerId = 'test-customer-id';
for (let i = 0; i < 420; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(420);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(400);
});
it('updates users about new messages in party', () => {
party.sendChat('message');
@@ -1074,6 +1101,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1130,6 +1158,7 @@ describe('Group Model', () => {
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expectedQuestMembers[sleepingParticipatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
@@ -1148,12 +1177,18 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
@@ -1172,9 +1207,11 @@ describe('Group Model', () => {
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1187,8 +1224,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.have.a.lengthOf(3);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
@@ -1202,6 +1240,13 @@ describe('Group Model', () => {
questStarted: true,
},
}];
sleepingParticipatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
@@ -1210,13 +1255,13 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
@@ -1226,6 +1271,8 @@ describe('Group Model', () => {
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else if (webhookOwner === sleepingParticipatingMember._id) {
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
@@ -1236,9 +1283,11 @@ describe('Group Model', () => {
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1252,14 +1301,17 @@ describe('Group Model', () => {
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1271,8 +1323,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
@@ -1281,7 +1334,7 @@ describe('Group Model', () => {
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
@@ -1346,6 +1399,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1368,7 +1422,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
@@ -1378,7 +1432,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
@@ -1386,7 +1440,7 @@ describe('Group Model', () => {
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@@ -1396,17 +1450,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1433,17 +1489,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.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 () => {
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
@@ -1470,13 +1528,16 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives xp and gold', async () => {
@@ -1485,15 +1546,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
});
context('drops', () => {
@@ -1593,13 +1658,16 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
it('sets user quest object to a clean state', async () => {
@@ -1632,7 +1700,7 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);
@@ -63,45 +63,48 @@ describe('GET /challenges/:challengeId', () => {
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'private'},
members: 1,
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the guild', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
@@ -114,53 +117,72 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => {
await challengeLeader.post(`/groups/${group._id}/leave`);
await challengeLeader.sync();
expect(challengeLeader.guilds).to.be.empty; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
context('party', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party'},
members: 1,
groupDetails: {type: 'party', privacy: 'private'},
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the party and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the party', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader.id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
id: group.id,
id: group._id,
categories: [],
name: group.name,
summary: group.name,
@@ -169,5 +191,21 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => {
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
});
@@ -1,6 +1,7 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -10,7 +11,7 @@ describe('GET /challenges/:challengeId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({ balance: 1 });
});
it('validates optional req.query.lastId to be an UUID', async () => {
@@ -21,7 +22,7 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if challenge doesn\'t exists', async () => {
it('fails if challenge doesn\'t exist', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -29,8 +30,8 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user);
it('fails if user isn\'t in the private group and isn\'t challenge leader', async () => {
let group = await generateGroup(user, {type: 'party', privacy: 'private'});
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
@@ -41,6 +42,27 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('works if user isn\'t in the private group but is challenge leader', async () => {
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party', privacy: 'private'},
members: 1,
});
let groupLeader = populatedGroup.groupLeader;
let challengeLeader = populatedGroup.members[0];
let challenge = await generateChallenge(challengeLeader, populatedGroup.group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let res = await challengeLeader.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
});
it('works with challenges belonging to public guild', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
@@ -94,16 +94,6 @@ describe('POST /challenges', () => {
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
@@ -46,7 +46,7 @@ describe('POST /challenges/:challengeId/join', () => {
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
it('returns an error when user isn\'t in the private group and isn\'t challenge leader', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
@@ -56,6 +56,16 @@ describe('POST /challenges/:challengeId/join', () => {
});
});
it('succeeds when user isn\'t in the private group but is challenge leader', async () => {
await groupLeader.post(`/challenges/${challenge._id}/leave`);
await groupLeader.post(`/groups/${group._id}/leave`);
await groupLeader.sync();
expect(groupLeader.guilds).to.be.empty; // check that leaving worked
let res = await groupLeader.post(`/challenges/${challenge._id}/join`);
expect(res.name).to.equal(challenge.name);
});
it('returns challenge data', async () => {
let res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
} from '../../../../helpers/api-integration/v3';
describe('GET /inbox/messages', () => {
let user;
@@ -22,17 +22,27 @@ describe('GET /inbox/messages', () => {
message: 'third',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
await user.sync();
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages.length).to.equal(Object.keys(user.inbox.messages).length);
expect(messages.length).to.equal(4);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
// message to yourself
expect(messages[0].text).to.equal('fourth');
expect(messages[0].sent).to.equal(false);
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('third');
expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first');
});
});
});
@@ -100,7 +100,7 @@ describe('POST /members/send-private-message', () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
@@ -116,6 +116,9 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(response.message.text).to.deep.equal(sendersMessageInSendersInbox.text);
expect(response.message.uuid).to.deep.equal(sendersMessageInSendersInbox.uuid);
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
@@ -6,7 +6,7 @@ import {
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
let endpoint = '/stripe/subscribe/cancel?noRedirect=true';
let user, group, stripeCancelSubscriptionStub;
beforeEach(async () => {
@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';
@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';
@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
@@ -5,7 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
@@ -1,7 +1,7 @@
import {
createAndPopulateGroup,
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
import {find} from 'lodash';
describe('PUT /tasks/:id', () => {
let user, guild, member, member2, task;
@@ -38,16 +38,64 @@ describe('PUT /tasks/:id', () => {
it('updates a group task', async () => {
let savedHabit = await user.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
expect(savedHabit.text).to.eql('some new text');
expect(savedHabit.notes).to.eql('some new notes');
expect(savedHabit.up).to.eql(false);
expect(savedHabit.down).to.eql(false);
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the todo
task = await member.put(`/tasks/${task._id}`, {
text: 'new text!',
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => memberTask.group.taskId === task._id);
// score up to trigger approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
});
it('updates a group task with checklist', async () => {
// add a new todo
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo',
type: 'todo',
checklist: [
{
text: 'checklist 1',
},
],
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
// change the checklist text
task = await user.put(`/tasks/${task._id}`, {
checklist: [
{
id: task.checklist[0].id,
text: 'checklist 1 - edit',
},
{
text: 'checklist 2 - edit',
},
],
});
expect(task.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {
@@ -3,25 +3,41 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('DELETE user message', () => {
let user;
let user, messagesId, otherUser;
beforeEach(async () => {
user = await generateUser({ inbox: { messages: { first: 'message', second: 'message' } } });
expect(user.inbox.messages.first).to.eql('message');
expect(user.inbox.messages.second).to.eql('message');
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
let userRes = await user.get('/user');
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('first');
});
it('one message', async () => {
let result = await user.del('/user/messages/first');
await user.sync();
expect(result).to.eql({ second: 'message' });
expect(user.inbox.messages).to.eql({ second: 'message' });
let result = await user.del(`/user/messages/${messagesId[0]}`);
messagesId = Object.keys(result);
expect(messagesId.length).to.eql(1);
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
});
it('clear all', async () => {
let result = await user.del('/user/messages');
await user.sync();
expect(user.inbox.messages).to.eql({});
let userRes = await user.get('/user');
expect(userRes.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});
+18 -2
View File
@@ -54,7 +54,7 @@ describe('PUT /user', () => {
});
it('profile.name cannot be an empty string or null', async () => {
it('validates profile.name', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
@@ -76,7 +76,23 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
message: t('invalidReqParams'),
});
await expect(user.put('/user', {
'profile.name': 'this is a very long display name that will not be allowed due to length',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueLength'),
});
await expect(user.put('/user', {
'profile.name': 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueSlur'),
});
});
});
@@ -41,6 +41,23 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('registers a new user and sets verifiedUsername to true', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.flags.verifiedUsername).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
@@ -39,7 +39,7 @@ describe('POST /user/auth/social', () => {
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
const response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
@@ -47,7 +47,10 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
expect(response.username).to.exist;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
});
it('logs an existing user in', async () => {
@@ -8,7 +8,11 @@ describe('POST /user/allocate', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -31,6 +35,16 @@ describe('POST /user/allocate', () => {
});
});
it('returns an error if the user hasn\'t selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 1});
let res = await user.post('/user/allocate?stat=con');
@@ -13,7 +13,11 @@ describe('POST /user/allocate-bulk', () => {
};
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -27,6 +31,16 @@ describe('POST /user/allocate-bulk', () => {
});
});
it('returns an error if user has not selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate-bulk', statsUpdate))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 3});
@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../helpers/api-integration/v4';
describe('POST /coupons/enter/:code', () => {
let user;
let sudoUser;
before(async () => {
await resetHabiticaDB();
});
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
});
});
it('returns an error if code is missing', async () => {
await expect(user.post('/coupons/enter')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
it('returns an error if code is invalid', async () => {
await expect(user.post('/coupons/enter/notValid')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidCoupon'),
});
});
it('returns an error if coupon has been used', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
await user.post(`/coupons/enter/${coupon._id}`); // use coupon
await expect(user.post(`/coupons/enter/${coupon._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('couponUsed'),
});
});
it('should apply the coupon to the user', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
let userRes = await user.post(`/coupons/enter/${coupon._id}`);
expect(userRes._id).to.equal(user._id);
expect(userRes.items.gear.owned.eyewear_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.eyewear_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_gold).to.be.true;
expect(userRes.extra).to.eql({signupEvent: 'wondercon'});
});
});
@@ -0,0 +1,30 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('DELETE /inbox/clear', () => {
it('removes all inbox messages for the user', async () => {
const [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
let messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
await user.del('/inbox/clear/');
messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(0);
});
});
@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /inbox/messages/:messageId', () => {
let user;
let otherUser;
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
});
it('returns an error if the messageId parameter is not an UUID', async () => {
await expect(user.del('/inbox/messages/123'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if the message does not exist', async () => {
await expect(user.del(`/inbox/messages/${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});
+58
View File
@@ -0,0 +1,58 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
import common from '../../../../website/common';
describe('GET /user', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the authenticated user with computed stats', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.stats.maxMP).to.exist;
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
});
it('does not return private paths (and apiToken)', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser.auth.local.hashed_password).to.not.exist;
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist;
});
it('returns only user properties requested', async () => {
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist;
});
it('does not return new inbox messages', async () => {
const otherUser = await generateUser();
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'second',
});
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.inbox.messages).to.be.undefined;
});
});
@@ -0,0 +1,324 @@
import {
generateUser,
translate as t,
createAndPopulateGroup,
generateGroup,
generateChallenge,
sleep,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
import apiError from '../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if spell does not exist', async () => {
await user.update({'stats.class': 'rogue'});
let spellId = 'invalidSpell';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell does not exist in user\'s class', async () => {
let spellId = 'pickPocket';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell.mana > user.mana', async () => {
await user.update({'stats.class': 'rogue'});
await expect(user.post('/user/class/cast/backStab'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughMana'),
});
});
it('returns an error if spell.value > user.gold', async () => {
await expect(user.post('/user/class/cast/birthday'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageNotEnoughGold'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellLevelTooHigh', {level: 13}),
});
});
it('returns an error if user doesn\'t own the spell', async () => {
await expect(user.post('/user/class/cast/snowball'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellNotOwned'),
});
});
it('returns an error if targetId is not an UUID', async () => {
await expect(user.post('/user/class/cast/spellId?targetId=notAnUUID'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if targetId is required but missing', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post('/user/class/cast/pickPocket'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('targetIdUUID'),
});
});
it('returns an error if targeted task doesn\'t exist', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post(`/user/class/cast/pickPocket?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('returns an error if a challenge task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'task text'},
]);
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupLeader.tasksOrder.habits[0]}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('challengeTasksNoCast'),
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'items.special.snowball': 3});
let target = generateUUID();
await expect(groupLeader.post(`/user/class/cast/snowball?targetId=${target}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: target}),
});
});
it('returns an error if party does not exists', async () => {
await user.update({'items.special.snowball': 3});
await expect(user.post(`/user/class/cast/snowball?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('partyNotFound'),
});
});
it('send message in party chat if party && !spell.silent', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
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 () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
});
it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/class/cast/brightness');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
expect(userChallengeTask.value).to.equal(0);
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
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
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'tasks\'');
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 === \'party\' and user is not in a party');
it('passes correct target to spell when targetType === \'user\' and user is not in a party');
});
@@ -0,0 +1,60 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/rebirth', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/rebirth'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 1.5,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
value: 1,
streak: 1,
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/rebirth');
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('rebirthComplete'));
expect(updatedDaily.streak).to.equal(0);
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});
+54
View File
@@ -0,0 +1,54 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/reroll', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/reroll'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 2,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/reroll');
await user.sync();
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('fortifyComplete'));
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});
+121
View File
@@ -0,0 +1,121 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../helpers/api-integration/v4';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
// More tests in common code unit tests
it('resets user\'s habits', async () => {
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.habits).to.be.empty;
});
it('resets user\'s dailys', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.dailys).to.be.empty;
});
it('resets user\'s todos', async () => {
let task = await user.post('/tasks/user', {
text: 'test todo',
type: 'todo',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.todos).to.be.empty;
});
it('resets user\'s rewards', async () => {
let task = await user.post('/tasks/user', {
text: 'test reward',
type: 'reward',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});
+256
View File
@@ -0,0 +1,256 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { each, get } from 'lodash';
describe('PUT /user', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
context('Allowed Operations', () => {
it('updates the user', async () => {
await user.put('/user', {
'profile.name': 'Frodo',
'preferences.costume': true,
'stats.hp': 14,
});
await user.sync();
expect(user.profile.name).to.eql('Frodo');
expect(user.preferences.costume).to.eql(true);
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 () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Top Level Protected Operations', () => {
let protectedOperations = {
'gem balance': {balance: 100},
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
contributor: {'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text'},
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
notifications: [{type: 123}],
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Sub-Level Protected Operations', () => {
let protectedOperations = {
'class stat': {'stats.class': 'wizard'},
'flags unless whitelisted': {'flags.dropsEnabled': true},
webhooks: {'preferences.webhooks': [1, 2, 3]},
sleep: {'preferences.sleep': true},
'disable classes': {'preferences.disableClasses': true},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Default Appearance Preferences', () => {
let testCases = {
shirt: 'yellow',
skin: 'ddc994',
'hair.color': 'blond',
'hair.bangs': 2,
'hair.base': 1,
'hair.flower': 4,
size: 'broad',
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`updates user with ${type} that is a default`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity checks to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('returns an error if user tries to update body size with invalid type', async () => {
await expect(user.put('/user', {
'preferences.size': 'round',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', { val: 'round', key: 'preferences.size' }),
});
});
it('can set beard to default', async () => {
await user.update({
'purchased.hair.beard': 3,
'preferences.hair.beard': 3,
});
let updatedUser = await user.put('/user', {
'preferences.hair.beard': 0,
});
expect(updatedUser.preferences.hair.beard).to.eql(0);
});
it('can set mustache to default', async () => {
await user.update({
'purchased.hair.mustache': 2,
'preferences.hair.mustache': 2,
});
let updatedUser = await user.put('/user', {
'preferences.hair.mustache': 0,
});
expect(updatedUser.preferences.hair.mustache).to.eql(0);
});
});
context('Purchasable Appearance Preferences', () => {
let testCases = {
background: 'volcano',
shirt: 'convict',
skin: 'cactus',
'hair.base': 7,
'hair.beard': 2,
'hair.color': 'rainbow',
'hair.mustache': 2,
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', {val: item, key: `preferences.${type}`}),
});
});
it(`updates user with ${type} user does own`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity check to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
});
context('Improvement Categories', () => {
it('sets valid categories', async () => {
await user.put('/user', {
'preferences.improvementCategories': ['work', 'school'],
});
await user.sync();
expect(user.preferences.improvementCategories).to.eql(['work', 'school']);
});
it('discards invalid categories', async () => {
await expect(user.put('/user', {
'preferences.improvementCategories': ['work', 'procrastination', 'school'],
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
});
@@ -0,0 +1,739 @@
import {
generateUser,
requester,
translate as t,
createAndPopulateGroup,
getProperty,
} from '../../../../helpers/api-integration/v4';
import { ApiUser } from '../../../../helpers/api-integration/api-classes';
import { v4 as uuid } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => {
context('username and email are free', () => {
let api;
beforeEach(async () => {
api = requester();
});
it('registers a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
let email = 'test@example.com';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username.trim());
expect(user.profile.name).to.eql(username.trim());
});
context('validates username', () => {
const email = 'test@example.com';
const password = 'password';
it('requires to username to be less than 20', async () => {
const username = (Date.now() + uuid()).substring(0, 21);
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
const username = 'a-zA_Z09*';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('allows only [-_a-zA-Z0-9] characters', async () => {
const username = 'a-zA_Z09';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username);
});
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
xit('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
});
context('does not provide default tags and tasks', async () => {
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
});
it('enrolls new users in an A/B test', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let confirmPassword = 'not password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a username', async () => {
let email = `${generateRandomUserName()}@example.com`;
let password = 'password';
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires an email', async () => {
let username = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a valid email', async () => {
let username = generateRandomUserName();
let email = 'notanemail@sdf';
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
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 () => {
let username = generateRandomUserName();
let email = `${username}@habitica.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('fails on a habitrpg.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitrpg.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('requires a password', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('attach to facebook user', () => {
let user;
let email = 'some@email.net';
let username = 'some-username';
let password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.facebook.id': 'some-fb-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username, email, api;
beforeEach(async () => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
return generateUser({
'auth.local.username': username,
'auth.local.lowerCaseUsername': username,
'auth.local.email': email,
});
});
it('rejects if username is already taken', async () => {
let uniqueEmail = `${generateRandomUserName()}@example.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email: uniqueEmail,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('usernameTaken'),
});
});
it('rejects if email is already taken', async () => {
let uniqueUsername = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username: uniqueUsername,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('emailTaken'),
});
});
});
context('req.query.groupInvite', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('does not crash the signup process when it\'s invalid', async () => {
let user = await api.post('/user/auth/local/register?groupInvite=aaaaInvalid', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.be.a('string');
});
it('supports invite using req.query.groupInvite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(), // so we can let it expire
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
await groupLeader.sync();
expect(groupLeader.achievements.invitedFriend).to.be.true;
});
it('user not added to a party on expired invite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now() - 6.912e8, // 8 days old
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all site tour values to -2 (already seen)', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tour, (value) => {
expect(value).to.eql(-2);
});
});
it('populates user with default todos, not no other task types', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.not.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
});
context('successful login with habitica-web header', () => {
let api, username, email, password;
beforeEach(() => {
api = requester({}, {'x-client': 'habitica-web'});
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all common tutorial flags to true', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tutorial.common, (value) => {
expect(value).to.eql(true);
});
});
it('populates user with default todos, habits, and rewards', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let todos = await requests.get('/tasks/user?type=todos');
expect(habits).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
});
});
});
@@ -0,0 +1,89 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
const ENDPOINT = '/user/auth/verify-username';
describe('POST /user/auth/verify-username', async () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('successfully verifies username', async () => {
let newUsername = 'new-username';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
it('successfully verifies username with allowed characters', async () => {
let newUsername = 'new-username_123';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
context('errors', async () => {
it('errors if username is not provided', async () => {
await expect(user.post(ENDPOINT, {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if username is a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username contains a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username is not allowed', async () => {
await expect(user.post(ENDPOINT, {
username: 'support',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username is not allowed regardless of casing', async () => {
await expect(user.post(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username has incorrect length', async () => {
await expect(user.post(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength')] });
});
it('errors if username contains invalid characters', async () => {
await expect(user.post(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: 'test.name',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: '🤬',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
});
});
});
@@ -0,0 +1,224 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
const ENDPOINT = '/user/auth/update-username';
describe('PUT /user/auth/update-username', async () => {
let user;
let password = 'password'; // from habitrpg/test/helpers/api-integration/v4/object-generators.js
beforeEach(async () => {
user = await generateUser();
});
it('successfully changes username with password', async () => {
let newUsername = 'new-username';
let response = await user.put(ENDPOINT, {
username: newUsername,
password,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username without password', async () => {
let newUsername = 'new-username-nopw';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username containing number and underscore', async () => {
let newUsername = 'new_username9';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('sets verifiedUsername when changing username', async () => {
user.flags.verifiedUsername = false;
await user.sync();
let newUsername = 'new-username-verify';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.flags.verifiedUsername).to.eql(true);
});
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let myNewUsername = 'my-new-username';
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
let response = await user.put(ENDPOINT, {
username: myNewUsername,
password: textPassword,
});
expect(response).to.eql({ username: myNewUsername });
await user.sync();
expect(user.auth.local.username).to.eql(myNewUsername);
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
context('errors', async () => {
it('prevents username update if new username is already taken', async () => {
let existingUsername = 'existing-username';
await generateUser({'auth.local.username': existingUsername, 'auth.local.lowerCaseUsername': existingUsername });
await expect(user.put(ENDPOINT, {
username: existingUsername,
password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameTaken'),
});
});
it('errors if password is wrong', async () => {
let newUsername = 'new-username';
await expect(user.put(ENDPOINT, {
username: newUsername,
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('errors if new username is not provided', async () => {
await expect(user.put(ENDPOINT, {
password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if new username is a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username contains a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username is not allowed', async () => {
await expect(user.put(ENDPOINT, {
username: 'support',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if new username is not allowed regardless of casing', async () => {
await expect(user.put(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if username has incorrect length', async () => {
await expect(user.put(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueLength'),
});
});
it('errors if new username contains invalid characters', async () => {
await expect(user.put(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: 'test.name',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: '🤬',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
});
});
});
@@ -0,0 +1,184 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import NotificationsComponent from 'client/components/notifications.vue';
import Store from 'client/libs/store';
import { hasClass } from 'client/store/getters/members';
import { toNextLevel } from 'common/script/statHelpers';
const localVue = createLocalVue();
localVue.use(Store);
describe('Notifications', () => {
let store;
let wrapper;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
stats: {
lvl: 0,
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'user:fetch': () => {},
'tasks:fetchUserTasks': () => {},
'snackbars:add': () => {},
},
getters: {
'members:hasClass': hasClass,
},
});
wrapper = shallowMount(NotificationsComponent, {
store,
localVue,
mocks: {
$t: (string) => string,
},
});
});
it('set user has class computed prop', () => {
expect(wrapper.vm.userHasClass).to.be.false;
store.state.user.data.stats.lvl = 10;
store.state.user.data.flags.classSelected = true;
store.state.user.data.preferences.disableClasses = false;
expect(wrapper.vm.userHasClass).to.be.true;
});
describe('user exp notifcation', () => {
it('notifies when user gets more exp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 10;
const userExpAfter = 12;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user levels with exact xp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expEarned;
const userExpAfter = 0;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user levels with exact more exp than needed', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10;
const expNeeded = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user has more exp than needed then levels', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10;
const expNeeded = -5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 15;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user multilevels', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 8;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10 + toNextLevel(userLevelBefore + 1);
const expNeeded = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user looses xp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 10;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user looses xp under 0', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 5;
const userExpAfter = -3;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user dies', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 10;
const userLevelAfter = 9;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = -20;
const userExpBefore = 20;
const userExpAfter = 0;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
});
});
@@ -1,4 +1,4 @@
import {shallow} from '@vue/test-utils';
import { shallow } from '@vue/test-utils';
import SidebarSection from 'client/components/sidebarSection.vue';
@@ -51,4 +51,4 @@ describe('Sidebar Section', () => {
expect(wrapper.find('.section-body').element.style.display).to.eq('none');
});
});
});
@@ -88,7 +88,7 @@ describe('Task Column', () => {
expect(el).to.eq(taskListOverride[i]);
});
wrapper.setProps({ isUser: false, taskListOverride });
wrapper.setProps({ isUser: false });
wrapper.vm.taskList.forEach((el, i) => {
expect(el).to.eq(taskListOverride[i]);
@@ -0,0 +1,19 @@
import generateStore from 'client/store';
describe('canDelete getter', () => {
it('cannot delete active challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2}};
expect(store.getters['tasks:canDelete'](task)).to.equal(false);
});
it('can delete broken challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2, broken: true}};
expect(store.getters['tasks:canDelete'](task)).to.equal(true);
});
});
@@ -0,0 +1,141 @@
import generateStore from 'client/store';
describe('getTaskClasses getter', () => {
let store, getTaskClasses;
beforeEach(() => {
store = generateStore();
store.state.user.data = {
preferences: {
},
};
getTaskClasses = store.getters['tasks:getTaskClasses'];
});
it('returns reward edit-modal-bg class', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns worst task edit-modal-bg class', () => {
const task = {type: 'todo', value: -21};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worst-modal-bg');
});
it('returns worse task edit-modal-bg class', () => {
const task = {type: 'todo', value: -11};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worse-modal-bg');
});
it('returns bad task edit-modal-bg class', () => {
const task = {type: 'todo', value: -6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-bad-modal-bg');
});
it('returns neutral task edit-modal-bg class', () => {
const task = {type: 'todo', value: 0};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-neutral-modal-bg');
});
it('returns good task edit-modal-bg class', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-good-modal-bg');
});
it('returns better task edit-modal-bg class', () => {
const task = {type: 'todo', value: 6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-better-modal-bg');
});
it('returns best task edit-modal-bg class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-best-modal-bg');
});
it('returns best task edit-modal-text class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-text')).to.equal('task-best-modal-text');
});
it('returns best task edit-modal-icon class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-icon')).to.equal('task-best-modal-icon');
});
it('returns best task edit-modal-option-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-option-disabled')).to.equal('task-best-modal-option-disabled');
});
it('returns best task edit-modal-control-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-habit-control-disabled')).to.equal('task-best-modal-habit-control-disabled');
});
it('returns create-modal-bg class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns create-modal-text class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-text')).to.equal('task-purple-modal-text');
});
it('returns create-modal-icon class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-icon')).to.equal('task-purple-modal-icon');
});
it('returns create-modal-option-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-option-disabled')).to.equal('task-purple-modal-option-disabled');
});
it('returns create-modal-habit-control-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-habit-control-disabled')).to.equal('task-purple-modal-habit-control-disabled');
});
it('returns completed todo classes', () => {
const task = {type: 'todo', value: 2, completed: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-disabled-daily-todo-control-bg',
checkbox: 'task-disabled-daily-todo-control-checkbox',
inner: 'task-disabled-daily-todo-control-inner',
content: 'task-disabled-daily-todo-control-content',
});
});
xit('returns good todo classes', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-good-control-bg',
checkbox: 'task-good-control-checkbox',
inner: 'task-good-control-inner-daily-todo`',
});
});
it('returns reward classes', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-reward-control-bg',
});
});
it('returns habit up classes', () => {
const task = {type: 'habit', value: 2, up: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
up: {
bg: 'task-good-control-bg',
inner: 'task-good-control-inner-habit',
},
down: {
bg: 'task-disabled-habit-control-bg',
inner: 'task-disabled-habit-control-inner',
},
});
});
});
+52
View File
@@ -0,0 +1,52 @@
import hasClass from '../../../website/common/script/libs/hasClass';
import { generateUser } from '../../helpers/common.helper';
describe('hasClass', () => {
it('returns false for user with level below 10', () => {
let userLvl9 = generateUser({
'stats.lvl': 9,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userLvl9);
expect(result).to.eql(false);
});
it('returns false for user with class not selected', () => {
let userClassNotSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': false,
'preferences.disableClasses': false,
});
let result = hasClass(userClassNotSelected);
expect(result).to.eql(false);
});
it('returns false for user with classes disabled', () => {
let userClassesDisabled = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': true,
});
let result = hasClass(userClassesDisabled);
expect(result).to.eql(false);
});
it('returns true for user with class', () => {
let userClassSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userClassSelected);
expect(result).to.eql(true);
});
});
-20
View File
@@ -1,20 +0,0 @@
import clearPMs from '../../../website/common/script/ops/clearPMs';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.clearPMs', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('clears messages', () => {
expect(user.inbox.messages).to.not.eql({});
let [result] = clearPMs(user);
expect(user.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});
-20
View File
@@ -1,20 +0,0 @@
import deletePM from '../../../website/common/script/ops/deletePM';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.deletePM', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('delete message', () => {
expect(user.inbox.messages).to.not.eql({ second: 'message' });
let [response] = deletePM(user, { params: { id: 'first' } });
expect(user.inbox.messages).to.eql({ second: 'message' });
expect(response).to.eql({ second: 'message' });
});
});
+38 -1
View File
@@ -13,7 +13,11 @@ describe('shared.ops.allocate', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -28,6 +32,39 @@ describe('shared.ops.allocate', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocate(user);
+59 -1
View File
@@ -13,7 +13,11 @@ describe('shared.ops.allocateBulk', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -43,6 +47,60 @@ describe('shared.ops.allocateBulk', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocateBulk(user, {
+42 -40
View File
@@ -15,21 +15,21 @@ div
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
template(v-if="isUserLoaded")
div.resting-banner(v-if="showRestingBanner")
div.resting-banner(v-show="showRestingBanner", ref="restingBanner")
span.content
span.label {{ $t('innCheckOutBanner') }}
span.separator |
span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }}
span.label.d-none.d-sm-inline {{ $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
app-menu(:class='{"restingInn": showRestingBanner}')
app-menu(:class='{"restingInn": showRestingBanner}' :style="{ marginTop: bannerHeight + 'px' }")
.container-fluid
app-header(:class='{"restingInn": showRestingBanner}')
buyModal(
:item="selectedItemToBuy || {}",
:withPin="true",
@change="resetItemToBuy($event)",
@buyPressed="customPurchase($event)",
:genericPurchase="genericPurchase(selectedItemToBuy)",
@@ -119,20 +119,9 @@ div
z-index: 1600 !important; /* Must stay above nav bar */
}
.restingInn {
.navbar {
top: 40px;
}
#app-header {
margin-top: 40px !important;
}
}
.resting-banner {
width: 100%;
height: 40px;
min-height: 40px;
background-color: $blue-10;
position: fixed;
top: 0;
@@ -140,11 +129,10 @@ div
display: flex;
.content {
height: 24px;
line-height: 1.71;
text-align: center;
color: $white;
padding: 8px 38px 8px 8px;
margin: auto;
}
@@ -161,6 +149,13 @@ div
}
}
@media only screen and (max-width: 768px) {
.content {
font-size: 12px;
line-height: 1.4;
}
}
.separator {
color: $blue-100;
margin: 0px 15px;
@@ -169,6 +164,7 @@ div
.resume {
font-weight: bold;
cursor: pointer;
white-space:nowrap;
}
}
</style>
@@ -224,6 +220,7 @@ export default {
loading: true,
currentTipNumber: 0,
bannerHidden: false,
bannerHeight: 0,
};
},
computed: {
@@ -332,23 +329,13 @@ export default {
if (notificationNotFoundMessage.indexOf(errorMessage) !== -1) snackbarTimeout = true;
let errorsToShow = [];
let usernameCheck = false;
let emailCheck = false;
let passwordCheck = false;
// show only the first error for each param
let paramErrorsFound = {};
if (errorData.errors) {
for (let e of errorData.errors) {
if (!usernameCheck && e.param === 'username') {
if (!paramErrorsFound[e.param]) {
errorsToShow.push(e.message);
usernameCheck = true;
}
if (!emailCheck && e.param === 'email') {
errorsToShow.push(e.message);
emailCheck = true;
}
if (!passwordCheck && e.param === 'password') {
errorsToShow.push(e.message);
passwordCheck = true;
paramErrorsFound[e.param] = true;
}
}
} else {
@@ -414,7 +401,6 @@ export default {
this.$store.watch(state => state.title, (title) => {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
@@ -432,6 +418,14 @@ export default {
this.hideLoadingScreen();
window.addEventListener('resize', this.setBannerOffset);
// Adjust the positioning of the header banners
this.$watch('showRestingBanner', () => {
this.$nextTick(() => {
this.setBannerOffset();
});
}, {immediate: true});
// Adjust the timezone offset
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
this.$store.dispatch('user:set', {
@@ -458,6 +452,7 @@ export default {
this.$root.$off('bv::show::modal');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
window.removeEventListener('resize', this.setBannerOffset);
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
@@ -567,13 +562,6 @@ export default {
});
}
},
resetItemToBuy ($event) {
// @TODO: Do we need this? I think selecting a new item
// overwrites. @negue might know
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
this.selectedItemToBuy = null;
}
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
@@ -623,10 +611,22 @@ export default {
},
hideBanner () {
this.bannerHidden = true;
this.setBannerOffset();
},
resumeDamage () {
this.$store.dispatch('user:sleep');
},
setBannerOffset () {
let contentPlacement = 0;
if (this.showRestingBanner && this.$refs.restingBanner !== undefined) {
contentPlacement = this.$refs.restingBanner.clientHeight;
}
this.bannerHeight = contentPlacement;
let smartBanner = document.getElementsByClassName('smartbanner')[0];
if (smartBanner !== undefined) {
smartBanner.style.top = `${contentPlacement}px`;
}
},
},
};
</script>
@@ -658,4 +658,6 @@ export default {
<style src="assets/css/sprites/spritesmith-main-20.css"></style>
<style src="assets/css/sprites/spritesmith-main-21.css"></style>
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites/spritesmith-main-23.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
@@ -1,42 +1,96 @@
.promo_armoire_backgrounds_201808 {
.achievement-costumeContest6x {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px 0px;
background-position: -1277px -296px;
width: 144px;
height: 156px;
}
.promo_alligator {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 480px;
height: 360px;
}
.promo_animal_tails {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -994px 0px;
width: 141px;
height: 441px;
}
.promo_armoire_backgrounds_201810 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -937px;
width: 423px;
height: 147px;
}
.promo_fall_avatar_customizations {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -394px -723px;
width: 336px;
height: 207px;
}
.promo_fall_festival_2017 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -481px -495px;
width: 414px;
height: 210px;
}
.promo_fall_festival_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -723px;
width: 393px;
height: 213px;
}
.promo_forest_friends_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px 0px;
width: 423px;
height: 147px;
}
.promo_ghost_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -424px -937px;
width: 423px;
height: 147px;
}
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px 0px;
background-position: 0px -361px;
width: 375px;
height: 361px;
}
.promo_kangaroo {
.promo_mystery_201809 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -327px;
width: 420px;
height: 336px;
background-position: -1136px -148px;
width: 306px;
height: 147px;
}
.promo_mystery_201807 {
.promo_seasonal_shop {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px -442px;
width: 114px;
height: 120px;
background-position: -1136px -591px;
width: 162px;
height: 138px;
}
.promo_spooky_sparkles {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -296px;
width: 140px;
height: 294px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px -563px;
background-position: -1443px -148px;
width: 96px;
height: 69px;
}
.promo_unconventional_armor {
.scene_nametag {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px -362px;
width: 180px;
height: 180px;
background-position: -481px 0px;
width: 512px;
height: 208px;
}
.scene_tavern {
.scene_tools {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 440px;
height: 326px;
background-position: -481px -209px;
width: 366px;
height: 285px;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,474 @@
.Pet-Unicorn-Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px 0px;
width: 81px;
height: 99px;
}
.Pet-Unicorn-Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px -400px;
width: 81px;
height: 99px;
}
.Pet-Unicorn-Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px 0px;
width: 81px;
height: 99px;
}
.Pet-Unicorn-White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -100px;
width: 81px;
height: 99px;
}
.Pet-Unicorn-Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px -100px;
width: 81px;
height: 99px;
}
.Pet-Whale-Base {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px -100px;
width: 81px;
height: 99px;
}
.Pet-Whale-CottonCandyBlue {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px 0px;
width: 81px;
height: 99px;
}
.Pet-Whale-CottonCandyPink {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px -100px;
width: 81px;
height: 99px;
}
.Pet-Whale-Desert {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -200px;
width: 81px;
height: 99px;
}
.Pet-Whale-Golden {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px -200px;
width: 81px;
height: 99px;
}
.Pet-Whale-Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px -200px;
width: 81px;
height: 99px;
}
.Pet-Whale-Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px -200px;
width: 81px;
height: 99px;
}
.Pet-Whale-Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px 0px;
width: 81px;
height: 99px;
}
.Pet-Whale-White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px -100px;
width: 81px;
height: 99px;
}
.Pet-Whale-Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Aquatic {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Base {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-CottonCandyBlue {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-CottonCandyPink {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Cupid {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Desert {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Ember {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Fairy {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Floral {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Ghost {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Glass {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Glow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Golden {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Holly {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Peppermint {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Rainbow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-RoyalPurple {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Shimmer {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px -400px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Spooky {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-StarryNight {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Thunderstorm {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Veteran {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -500px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -82px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Base {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -164px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-CottonCandyBlue {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -246px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-CottonCandyPink {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -328px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Desert {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -410px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Golden {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -492px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -574px -500px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px 0px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px -100px;
width: 81px;
height: 99px;
}
.Pet-Yarn-White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px -200px;
width: 81px;
height: 99px;
}
.Pet-Yarn-Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px -300px;
width: 81px;
height: 99px;
}
.Pet_HatchingPotion_Aquatic {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px -469px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Base {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_CottonCandyBlue {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_CottonCandyPink {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Cupid {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Desert {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Ember {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -276px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Fairy {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -345px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Floral {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -414px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Ghost {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -483px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Glass {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -552px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Glow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -621px -600px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Golden {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Holly {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -656px -400px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Peppermint {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Purple {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Rainbow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -276px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -345px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_RoyalPurple {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -414px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -483px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Shimmer {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -552px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -621px -669px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Spooky {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -738px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_StarryNight {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -738px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Thunderstorm {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -738px -138px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -738px -207px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -738px -276px;
width: 68px;
height: 68px;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 155 KiB

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