mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-26 10:42:52 +01:00
Compare commits
300 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4fc6671b4 | ||
|
|
e7a096158e | ||
|
|
98473fcfaa | ||
|
|
e4300fc714 | ||
|
|
ffba435923 | ||
|
|
1f44444a50 | ||
|
|
061d990e39 | ||
|
|
71f4e6bc08 | ||
|
|
659f160e22 | ||
|
|
5f27bc5f90 | ||
|
|
074837b274 | ||
|
|
aa517e0ad6 | ||
|
|
5ca489dee7 | ||
|
|
fe39ef72ff | ||
|
|
eee5f2f1df | ||
|
|
fd8572c28a | ||
|
|
f161987e1e | ||
|
|
2304d970a5 | ||
|
|
25ed05ab0a | ||
|
|
6f5b9ef119 | ||
|
|
c64ea0a9a9 | ||
|
|
2e36b896d4 | ||
|
|
6fe73d431e | ||
|
|
998621cefe | ||
|
|
67bb179c25 | ||
|
|
c875861dab | ||
|
|
418c18ddb2 | ||
|
|
0caab5c8d0 | ||
|
|
218e65b04b | ||
|
|
fcd7ba77a7 | ||
|
|
b0d177643c | ||
|
|
c0e0b10a95 | ||
|
|
0bee2caf2e | ||
|
|
e56d097b3a | ||
|
|
8c63a9e31f | ||
|
|
28ed9d8bcc | ||
|
|
36ead77e0c | ||
|
|
e7969987ec | ||
|
|
97021e3422 | ||
|
|
218d47d64a | ||
|
|
bdfc23717e | ||
|
|
464cd87736 | ||
|
|
67a8eebb96 | ||
|
|
cfc0f6a3ac | ||
|
|
9f76db12bd | ||
|
|
70192e4935 | ||
|
|
5cd4ead9d1 | ||
|
|
87cd000bb8 | ||
|
|
0de5d8273b | ||
|
|
379898cc4d | ||
|
|
adeaa6c754 | ||
|
|
539f0e33e2 | ||
|
|
405e053377 | ||
|
|
52fbb8f899 | ||
|
|
c880596a77 | ||
|
|
a35f04be46 | ||
|
|
8682cf1cf7 | ||
|
|
6e922cfb44 | ||
|
|
cafabd93e1 | ||
|
|
1001d48eb7 | ||
|
|
b5c4618d56 | ||
|
|
92a4ba93d2 | ||
|
|
90d35d2f1f | ||
|
|
fead027cd2 | ||
|
|
5578426985 | ||
|
|
1c39fae127 | ||
|
|
45a757b589 | ||
|
|
8b610d771c | ||
|
|
bd81d27145 | ||
|
|
8eb430cbcb | ||
|
|
f218133d25 | ||
|
|
5f440d9097 | ||
|
|
0294868747 | ||
|
|
9c8d870d16 | ||
|
|
a7acd863f3 | ||
|
|
f32ef0a6ba | ||
|
|
ebf3b4aa47 | ||
|
|
5a8366468b | ||
|
|
df57518815 | ||
|
|
7d342b5115 | ||
|
|
388de9a97d | ||
|
|
28c79d9d20 | ||
|
|
85cf322b30 | ||
|
|
362ca73c94 | ||
|
|
5632031f16 | ||
|
|
90273362c4 | ||
|
|
7aadc10fab | ||
|
|
bfd45596b5 | ||
|
|
2eed4d38ae | ||
|
|
29bbe8534b | ||
|
|
9e008890b2 | ||
|
|
5505bf1e45 | ||
|
|
d40781ce07 | ||
|
|
d9719cdc05 | ||
|
|
8cc6a96be0 | ||
|
|
c5fb2d6506 | ||
|
|
afc336461e | ||
|
|
31376c8461 | ||
|
|
3a849bac18 | ||
|
|
563f3e2012 | ||
|
|
e24a024091 | ||
|
|
dc7d3816fd | ||
|
|
a094e13352 | ||
|
|
83376a38de | ||
|
|
db9c13a05d | ||
|
|
8c8aa78a1a | ||
|
|
6e3f7c005a | ||
|
|
1395380dfe | ||
|
|
833ceb3bf3 | ||
|
|
0522aa1551 | ||
|
|
58a9e4a439 | ||
|
|
84e2b2f45e | ||
|
|
71c0939a15 | ||
|
|
26c8323e70 | ||
|
|
bb7d447003 | ||
|
|
97ea510a34 | ||
|
|
99610b4916 | ||
|
|
9a43b85492 | ||
|
|
ecbf39cee4 | ||
|
|
4394772ee3 | ||
|
|
dd7fa73961 | ||
|
|
6ec23ce790 | ||
|
|
b953519e2d | ||
|
|
33a8072d23 | ||
|
|
213316d807 | ||
|
|
44cd4d0708 | ||
|
|
063b7a9af0 | ||
|
|
c08b5a4f1e | ||
|
|
90117625d7 | ||
|
|
1b3dad749e | ||
|
|
a622a3ebe3 | ||
|
|
2c83c16644 | ||
|
|
1034675184 | ||
|
|
a420876697 | ||
|
|
dc265e26b3 | ||
|
|
b5203dda61 | ||
|
|
80e92a8767 | ||
|
|
a265bfac9d | ||
|
|
92e4d5cd68 | ||
|
|
a18e9b3b18 | ||
|
|
c1a6ba6242 | ||
|
|
ed761a8b7b | ||
|
|
81d5971829 | ||
|
|
eb2d320d1f | ||
|
|
67538a368e | ||
|
|
d55b95834d | ||
|
|
9ff9cd3b35 | ||
|
|
b1f24de3c4 | ||
|
|
2ce2100f89 | ||
|
|
dbaae4183e | ||
|
|
2009bb97cb | ||
|
|
3fc9501bac | ||
|
|
2c2ded2b70 | ||
|
|
d689010e38 | ||
|
|
e173b7784c | ||
|
|
c3db59aae8 | ||
|
|
44e063c035 | ||
|
|
4e2c08cfed | ||
|
|
c845c337df | ||
|
|
418b57f9fb | ||
|
|
9725da258e | ||
|
|
4191ea1968 | ||
|
|
a9340ee60f | ||
|
|
c8d874d28a | ||
|
|
32a22f1545 | ||
|
|
8ffe302a49 | ||
|
|
5c50a40f39 | ||
|
|
84329e5fad | ||
|
|
64507a161e | ||
|
|
0f7fc27663 | ||
|
|
1545685a5b | ||
|
|
410355c3f1 | ||
|
|
ac27cabf6a | ||
|
|
972631e7ac | ||
|
|
d27ed7c406 | ||
|
|
031783b1d7 | ||
|
|
318aa7cbd9 | ||
|
|
f802a41f75 | ||
|
|
1d597039ca | ||
|
|
07bc374078 | ||
|
|
8153674dc0 | ||
|
|
0002148326 | ||
|
|
d198e23de6 | ||
|
|
4f4e141806 | ||
|
|
05e8d6f032 | ||
|
|
39847893d2 | ||
|
|
e4dbf09dda | ||
|
|
eb99b709e0 | ||
|
|
862b3453f8 | ||
|
|
7f48853d32 | ||
|
|
5c4f763bb1 | ||
|
|
bc9401b2f7 | ||
|
|
6fb9030b96 | ||
|
|
ba307af963 | ||
|
|
cf4b920a67 | ||
|
|
b0ff35a8f1 | ||
|
|
85b4c7825e | ||
|
|
5b7ea8ec5c | ||
|
|
5cfd0c863e | ||
|
|
10c6244c0c | ||
|
|
20e65be8bf | ||
|
|
8bac324ba7 | ||
|
|
2ee0288aaa | ||
|
|
b7ef4c50b2 | ||
|
|
52be9c750f | ||
|
|
b0200026aa | ||
|
|
e6c8b977c8 | ||
|
|
c78b5ecf7c | ||
|
|
f27e9b02d8 | ||
|
|
c06c19ca41 | ||
|
|
d5d894b8a9 | ||
|
|
7bd4e6a5a9 | ||
|
|
f13eed5663 | ||
|
|
a9a2fe6314 | ||
|
|
d6514bce8b | ||
|
|
c862bdb76a | ||
|
|
b596576c53 | ||
|
|
603fc8c4dd | ||
|
|
3c602351f9 | ||
|
|
29ed33461c | ||
|
|
fbc1044100 | ||
|
|
35e02d2871 | ||
|
|
6aa204c3f5 | ||
|
|
eaaa5ad7f3 | ||
|
|
54468ff499 | ||
|
|
53405aa586 | ||
|
|
7630c02e13 | ||
|
|
ec444384f4 | ||
|
|
cce9b33844 | ||
|
|
b977d42402 | ||
|
|
2672cbd790 | ||
|
|
b7ca5be6ee | ||
|
|
5ae89761b0 | ||
|
|
8b0101c74c | ||
|
|
dbd295e35b | ||
|
|
b5428f4ac9 | ||
|
|
5b213b4f94 | ||
|
|
0142e332e8 | ||
|
|
b4f955333b | ||
|
|
696121fb24 | ||
|
|
2a7dfff88a | ||
|
|
2c921609c1 | ||
|
|
1f7dd421d4 | ||
|
|
45ca090105 | ||
|
|
02b22170e2 | ||
|
|
1134c7748b | ||
|
|
7019e32eed | ||
|
|
485c528b45 | ||
|
|
f1c1ba8efa | ||
|
|
b0e4c2cb11 | ||
|
|
0e346f7050 | ||
|
|
1eb1fe76a8 | ||
|
|
72a0e05804 | ||
|
|
5f37b9727a | ||
|
|
bf17b49046 | ||
|
|
36edf5265f | ||
|
|
6da243e034 | ||
|
|
b87ff03210 | ||
|
|
9d994f8a77 | ||
|
|
75c3f7214b | ||
|
|
f3f8fa3a42 | ||
|
|
2e34dab9a6 | ||
|
|
0f9b274059 | ||
|
|
041bde0cba | ||
|
|
8888e63005 | ||
|
|
7aa2fac14a | ||
|
|
4493e1d98c | ||
|
|
fcbc2acda7 | ||
|
|
729ba36ed3 | ||
|
|
896495cac5 | ||
|
|
3f89dae8c9 | ||
|
|
4ec5df170c | ||
|
|
fdbcd99525 | ||
|
|
99726bdc2f | ||
|
|
b00d1a067e | ||
|
|
0910ca7470 | ||
|
|
24f5e7c19f | ||
|
|
4f34443b84 | ||
|
|
714706f925 | ||
|
|
0899dddb42 | ||
|
|
33149e1afa | ||
|
|
c8becbccb5 | ||
|
|
c9465cbfdd | ||
|
|
965b7a3be7 | ||
|
|
6b8784cf04 | ||
|
|
508d832d73 | ||
|
|
734e4a963f | ||
|
|
40495aaacb | ||
|
|
5566460541 | ||
|
|
774a1d9a96 | ||
|
|
60df912dcc | ||
|
|
7325bc0871 | ||
|
|
2fc233e70f | ||
|
|
9fd26a88ea | ||
|
|
76860fe3f8 | ||
|
|
b16e700de5 | ||
|
|
f30d4b2cbf | ||
|
|
a31f1f19fc | ||
|
|
9b69b640c3 | ||
|
|
b75e65f42d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,6 +39,7 @@ dist-client
|
||||
test/client/unit/coverage
|
||||
test/client/e2e/reports
|
||||
test/client-old/spec/mocks/translations.js
|
||||
yarn.lock
|
||||
|
||||
# Elastic Beanstalk Files
|
||||
.elasticbeanstalk/*
|
||||
|
||||
@@ -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",
|
||||
|
||||
48
database_reports/20181001_backtoschool_challenge.js
Normal file
48
database_reports/20181001_backtoschool_challenge.js
Normal file
@@ -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;
|
||||
@@ -2,9 +2,13 @@ version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
server:
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
147
migrations/archive/2018/20180811_inboxOutsideUser.js
Normal file
147
migrations/archive/2018/20180811_inboxOutsideUser.js
Normal file
@@ -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;
|
||||
@@ -17,5 +17,12 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
const processUsers = require('./tasks/habits-one-history-entry-per-day-challenges.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
migrations/users/20181002_username_email.js
Normal file
107
migrations/users/20181002_username_email.js
Normal 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
migrations/users/generate-usernames.js
Normal file
99
migrations/users/generate-usernames.js
Normal 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;
|
||||
@@ -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 });
|
||||
|
||||
@@ -15,7 +15,6 @@ function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
migration: {$ne: migrationName},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2018-07-01')}, // rerun without date restriction after initial run
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
let migrationName = '20180702_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: ['f0481f95-1dde-4ae7-a876-d19502a45d61']}, // Update per month
|
||||
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
|
||||
16484
package-lock.json
generated
16484
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.55.0",
|
||||
"version": "4.65.5",
|
||||
"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": "^2.0.0",
|
||||
"bcrypt": "^3.0.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"bootstrap": "^4.1.1",
|
||||
"bootstrap-vue": "^2.0.0-rc.9",
|
||||
@@ -35,7 +35,7 @@
|
||||
"coupon-code": "^0.4.5",
|
||||
"cross-env": "^5.1.5",
|
||||
"css-loader": "^0.28.11",
|
||||
"csv-stringify": "^2.1.0",
|
||||
"csv-stringify": "^3.0.0",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"express": "^4.16.3",
|
||||
@@ -43,7 +43,7 @@
|
||||
"express-validator": "^5.2.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"got": "^8.3.1",
|
||||
"got": "^9.0.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-imagemin": "^4.1.0",
|
||||
@@ -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",
|
||||
@@ -95,7 +97,7 @@
|
||||
"url-loader": "^1.0.0",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^9.4.1",
|
||||
"validator": "^10.5.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vue": "^2.5.16",
|
||||
"vue-loader": "^14.2.2",
|
||||
@@ -163,19 +165,19 @@
|
||||
"expect.js": "^0.3.1",
|
||||
"http-proxy-middleware": "^0.18.0",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^2.0.2",
|
||||
"karma": "^3.0.0",
|
||||
"karma-babel-preprocessor": "^7.0.0",
|
||||
"karma-chai-plugins": "^0.9.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-sinon-chai": "^1.3.4",
|
||||
"karma-sinon-chai": "^2.0.0",
|
||||
"karma-sinon-stub-promise": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.32",
|
||||
"karma-webpack": "^3.0.0",
|
||||
"lcov-result-merger": "^2.0.0",
|
||||
"lcov-result-merger": "^3.0.0",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^6.0.6",
|
||||
"nightwatch": "^0.9.21",
|
||||
|
||||
88
scripts/gdpr-delete-users.js
Normal file
88
scripts/gdpr-delete-users.js
Normal 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;
|
||||
@@ -65,6 +65,12 @@ describe('cron', () => {
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -655,76 +661,6 @@ describe('cron', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('user is sleeping', () => {
|
||||
beforeEach(() => {
|
||||
user.preferences.sleep = true;
|
||||
});
|
||||
|
||||
it('calls analytics', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('clears user buffs', () => {
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 1,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('resets all dailies without damaging user', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
let healthBefore = user.stats.hp;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
expect(user.stats.hp).to.equal(healthBefore);
|
||||
});
|
||||
|
||||
it('sets isDue for daily', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].isDue).to.be.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos', () => {
|
||||
beforeEach(() => {
|
||||
let todo = {
|
||||
@@ -846,6 +782,15 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
it('computes isDue when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
|
||||
it('computes nextDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
@@ -865,6 +810,13 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should set tasks completed to false when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for completed dailys', () => {
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
tasksByType.dailys[0].completed = true;
|
||||
@@ -872,6 +824,14 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for completed dailys when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
tasksByType.dailys[0].completed = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for dailys with scheduled misses', () => {
|
||||
daysMissed = 10;
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
@@ -884,12 +844,19 @@ describe('cron', () => {
|
||||
daysMissed = 1;
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('should not do damage for missing a daily when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
|
||||
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
@@ -930,7 +897,7 @@ describe('cron', () => {
|
||||
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
|
||||
});
|
||||
|
||||
it('should decrement quest progress down for missing a daily', () => {
|
||||
it('should decrement quest.progress.down for missing a daily', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
@@ -939,6 +906,16 @@ describe('cron', () => {
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
|
||||
it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let progress = cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
});
|
||||
|
||||
it('should do damage for only yesterday\'s dailies', () => {
|
||||
daysMissed = 3;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
@@ -1017,7 +994,7 @@ describe('cron', () => {
|
||||
expect(tasksByType.habits[0].counterDown).to.equal(0);
|
||||
});
|
||||
|
||||
it('should reset habit counters even if user is resting in the Inn', () => {
|
||||
it('should reset habit counters even if user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.habits[0].counterUp = 1;
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
@@ -1278,7 +1255,23 @@ describe('cron', () => {
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments user buffs if all (at least 1) due dailies were completed', () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
||||
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
||||
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
||||
});
|
||||
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
@@ -1317,6 +1310,31 @@ describe('cron', () => {
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({days: 1});
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 0,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
@@ -1341,7 +1359,50 @@ describe('cron', () => {
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('still grants a perfect day when CRON_SAFE_MODE is set', () => {
|
||||
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 0,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('always grants a perfect day buff when CRON_SAFE_MODE is set', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
||||
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
||||
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
||||
});
|
||||
|
||||
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
daysMissed = 1;
|
||||
@@ -1373,6 +1434,20 @@ describe('cron', () => {
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('should not add mp to user when user is sleeping', () => {
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
|
||||
user.preferences.sleep = true;
|
||||
let mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100}));
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', () => {
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
@@ -1514,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', () => {
|
||||
@@ -1568,7 +1622,7 @@ describe('cron', () => {
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('slack', () => {
|
||||
title: 'Flag in Some group - (private guild)',
|
||||
title_link: undefined,
|
||||
text: 'some text',
|
||||
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
|
||||
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message.>/),
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1864,28 +1932,54 @@ describe('Group Model', () => {
|
||||
|
||||
context('hasNotCancelled', () => {
|
||||
it('returns false if group does not have customer id', () => {
|
||||
expect(party.hasNotCancelled()).to.be.undefined;
|
||||
expect(party.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns true if party does not have plan.dateTerminated', () => {
|
||||
it('returns true if group does not have plan.dateTerminated', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if party if plan.dateTerminated is after today', () => {
|
||||
it('returns false if group if plan.dateTerminated is after today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if party if plan.dateTerminated is before today', () => {
|
||||
it('returns false if group if plan.dateTerminated is before today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
context('hasCancelled', () => {
|
||||
it('returns false if group does not have customer id', () => {
|
||||
expect(party.hasCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if group does not have plan.dateTerminated', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(party.hasCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns true if group if plan.dateTerminated is after today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(party.hasCancelled()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if group if plan.dateTerminated is before today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(party.hasCancelled()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,9 +315,8 @@ describe('User Model', () => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
|
||||
it('returns false if user does not have customer id', () => {
|
||||
expect(user.hasNotCancelled()).to.be.undefined;
|
||||
expect(user.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns true if user does not have plan.dateTerminated', () => {
|
||||
@@ -341,6 +340,38 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
context('hasCancelled', () => {
|
||||
let user;
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('returns false if user does not have customer id', () => {
|
||||
expect(user.hasCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if user does not have plan.dateTerminated', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(user.hasCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns true if user if plan.dateTerminated is after today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(user.hasCancelled()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if user if plan.dateTerminated is before today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(user.hasCancelled()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
context('pre-save hook', () => {
|
||||
it('does not try to award achievements when achievements or items not selected in query', async () => {
|
||||
let user = new User();
|
||||
|
||||
@@ -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,
|
||||
@@ -304,14 +294,14 @@ describe('POST /challenges', () => {
|
||||
expect(groupLeader.challenges.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('awards achievement if this is creator\'s first challenge', async () => {
|
||||
it('does not award joinedChallenge achievement for creating a challenge', async () => {
|
||||
await groupLeader.post('/challenges', {
|
||||
group: group._id,
|
||||
name: 'Test Challenge',
|
||||
shortName: 'TC Label',
|
||||
});
|
||||
groupLeader = await groupLeader.sync();
|
||||
expect(groupLeader.achievements.joinedChallenge).to.be.true;
|
||||
expect(groupLeader.achievements.joinedChallenge).to.not.be.true;
|
||||
});
|
||||
|
||||
it('sets summary to challenges name when not supplied', async () => {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -3,15 +3,23 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
|
||||
describe('POST /chat/:chatId/flag', () => {
|
||||
let user, admin, anotherUser, group;
|
||||
let user, admin, anotherUser, newUser, group;
|
||||
const TEST_MESSAGE = 'Test Message';
|
||||
const USER_AGE_FOR_FLAGGING = 3;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
user = await generateUser({balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
|
||||
admin = await generateUser({balance: 1, 'contributor.admin': true});
|
||||
anotherUser = await generateUser();
|
||||
anotherUser = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
|
||||
newUser = await generateUser({'auth.timestamps.created': moment().subtract(1, 'days').toDate()});
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
|
||||
group = await user.post('/groups', {
|
||||
name: 'Test Guild',
|
||||
@@ -20,6 +28,10 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('Returns an error when chat message is not found', async () => {
|
||||
await expect(user.post(`/groups/${group._id}/chat/incorrectMessage/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
@@ -34,7 +46,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
await expect(user.post(`/groups/${group._id}/chat/${message.message.id}/flag`)).to.eventually.be.ok;
|
||||
});
|
||||
|
||||
it('Flags a chat', async () => {
|
||||
it('Flags a chat and sends normal message to moderator Slack when user is not new', async () => {
|
||||
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
let flagResult = await user.post(`/groups/${group._id}/chat/${message.id}/flag`);
|
||||
@@ -45,6 +57,62 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
expect(messageToCheck.flags[user._id]).to.equal(true);
|
||||
|
||||
// Slack message to mods
|
||||
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: `${user.profile.name} (${user.id}; language: en) flagged a message`,
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`,
|
||||
title: 'Flag in Test Guild',
|
||||
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
||||
text: TEST_MESSAGE,
|
||||
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
/* eslint-ensable camelcase */
|
||||
});
|
||||
|
||||
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
|
||||
let automatedComment = `The post's flag count has not been increased because the flagger's account is less than ${USER_AGE_FOR_FLAGGING} days old.`;
|
||||
let { message } = await newUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
let flagResult = await newUser.post(`/groups/${group._id}/chat/${message.id}/flag`);
|
||||
expect(flagResult.flags[newUser._id]).to.equal(true);
|
||||
expect(flagResult.flagCount).to.equal(0);
|
||||
|
||||
let groupWithFlags = await admin.get(`/groups/${group._id}`);
|
||||
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
expect(messageToCheck.flags[newUser._id]).to.equal(true);
|
||||
|
||||
// Slack message to mods
|
||||
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`,
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`,
|
||||
title: 'Flag in Test Guild',
|
||||
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
||||
text: TEST_MESSAGE,
|
||||
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
/* eslint-ensable camelcase */
|
||||
});
|
||||
|
||||
it('Flags a chat when the author\'s account was deleted', async () => {
|
||||
@@ -117,7 +185,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when user tries to flag a message that is already flagged', async () => {
|
||||
it('Returns an error when user tries to flag a message that they already flagged', async () => {
|
||||
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
await user.post(`/groups/${group._id}/chat/${message.id}/flag`);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
generateUser,
|
||||
@@ -15,8 +17,6 @@ import { getMatchesByWordArray } from '../../../../../website/server/libs/string
|
||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
|
||||
@@ -80,12 +80,14 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
describe('mute user', () => {
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,7 +261,6 @@ describe('POST /chat', () => {
|
||||
title: 'Slur in Test Guild',
|
||||
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
|
||||
text: testSlurMessage,
|
||||
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
@@ -274,6 +275,7 @@ describe('POST /chat', () => {
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
|
||||
// @TODO: The next test should not depend on this. We should reset the user test in a beforeEach
|
||||
// Restore chat privileges to continue testing
|
||||
user.flags.chatRevoked = false;
|
||||
await user.update({'flags.chatRevoked': false});
|
||||
@@ -312,7 +314,6 @@ describe('POST /chat', () => {
|
||||
title: 'Slur in Party - (private party)',
|
||||
title_link: undefined,
|
||||
text: testSlurMessage,
|
||||
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
|
||||
@@ -4,23 +4,24 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import config from '../../../../../config.json';
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
const USER_AGE_FOR_FLAGGING = 3;
|
||||
let groupWithChat, message, author, nonAdmin, admin;
|
||||
|
||||
before(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupWithChat = group;
|
||||
author = groupLeader;
|
||||
nonAdmin = members[0];
|
||||
nonAdmin = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
|
||||
admin = await generateUser({'contributor.admin': true});
|
||||
|
||||
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
|
||||
@@ -69,9 +70,14 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
privateMessage = privateMessage.message;
|
||||
|
||||
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
|
||||
|
||||
// first test that the flag was actually successful
|
||||
let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
expect(messages[0].flagCount).to.eql(5);
|
||||
|
||||
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
|
||||
|
||||
let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
@@ -98,7 +98,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => {
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('Prevent multiple notifications', () => {
|
||||
let partyLeader, partyMembers, party;
|
||||
|
||||
before(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 4,
|
||||
});
|
||||
|
||||
party = group;
|
||||
partyLeader = groupLeader;
|
||||
partyMembers = members;
|
||||
});
|
||||
|
||||
it('does not add the same notification twice', async () => {
|
||||
const multipleChatMessages = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex++) {
|
||||
multipleChatMessages.push(
|
||||
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}`}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(multipleChatMessages);
|
||||
|
||||
const userWithNotification = await partyLeader.get('/user');
|
||||
|
||||
expect(userWithNotification.notifications.length).to.be.eq(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({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,21 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if use Healing Light spell with full health', async () => {
|
||||
await user.update({
|
||||
'stats.class': 'healer',
|
||||
'stats.lvl': 11,
|
||||
'stats.hp': 50,
|
||||
'stats.mp': 200,
|
||||
});
|
||||
await expect(user.post('/user/class/cast/heal'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageHealthAlreadyMax'),
|
||||
});
|
||||
});
|
||||
|
||||
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'))
|
||||
|
||||
@@ -50,11 +50,24 @@ describe('POST /user/push-devices', () => {
|
||||
});
|
||||
|
||||
it('adds a push device to the user', async () => {
|
||||
let response = await user.post('/user/push-devices', {type, regId});
|
||||
const response = await user.post('/user/push-devices', {type, regId});
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('pushDeviceAdded'));
|
||||
expect(response.data[0].type).to.equal(type);
|
||||
expect(response.data[0].regId).to.equal(regId);
|
||||
expect(user.pushDevices[0].type).to.equal(type);
|
||||
expect(user.pushDevices[0].regId).to.equal(regId);
|
||||
});
|
||||
|
||||
it('removes a push device to the user', async () => {
|
||||
await user.post('/user/push-devices', {type, regId});
|
||||
|
||||
const response = await user.del(`/user/push-devices/${regId}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('pushDeviceRemoved'));
|
||||
expect(response.data[0]).to.not.exist;
|
||||
expect(user.pushDevices[0]).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ';
|
||||
@@ -259,7 +276,7 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('enrolls new users in an A/B test', async () => {
|
||||
xit('enrolls new users in an A/B test', async () => {
|
||||
let username = generateRandomUserName();
|
||||
let email = `${username}@example.com`;
|
||||
let password = 'password';
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -77,7 +80,7 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
xit('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
@@ -133,7 +136,7 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
xit('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
|
||||
@@ -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});
|
||||
|
||||
|
||||
62
test/api/v4/coupon/POST-coupons_enter_code.test.js
Normal file
62
test/api/v4/coupon/POST-coupons_enter_code.test.js
Normal file
@@ -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'});
|
||||
});
|
||||
});
|
||||
30
test/api/v4/inbox/DELETE-inbox_clear.test.js
Normal file
30
test/api/v4/inbox/DELETE-inbox_clear.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
62
test/api/v4/inbox/DELETE-inbox_messages_messageId.test.js
Normal file
62
test/api/v4/inbox/DELETE-inbox_messages_messageId.test.js
Normal file
@@ -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
test/api/v4/user/GET-user.test.js
Normal file
58
test/api/v4/user/GET-user.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
324
test/api/v4/user/POST-user_class_cast_spellId.test.js
Normal file
324
test/api/v4/user/POST-user_class_cast_spellId.test.js
Normal file
@@ -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');
|
||||
});
|
||||
60
test/api/v4/user/POST-user_rebirth.test.js
Normal file
60
test/api/v4/user/POST-user_rebirth.test.js
Normal file
@@ -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
test/api/v4/user/POST-user_reroll.test.js
Normal file
54
test/api/v4/user/POST-user_reroll.test.js
Normal 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
test/api/v4/user/POST-user_reset.test.js
Normal file
121
test/api/v4/user/POST-user_reset.test.js
Normal 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
test/api/v4/user/PUT-user.test.js
Normal file
256
test/api/v4/user/PUT-user.test.js
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
739
test/api/v4/user/auth/POST-register_local.test.js
Normal file
739
test/api/v4/user/auth/POST-register_local.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
89
test/api/v4/user/auth/POST-user_verify_username.test.js
Normal file
89
test/api/v4/user/auth/POST-user_verify_username.test.js
Normal file
@@ -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')] });
|
||||
});
|
||||
});
|
||||
});
|
||||
224
test/api/v4/user/auth/PUT-user_update_username.test.js
Normal file
224
test/api/v4/user/auth/PUT-user_update_username.test.js
Normal file
@@ -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'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
184
test/client/unit/specs/components/notifications.js
Normal file
184
test/client/unit/specs/components/notifications.js
Normal file
@@ -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]);
|
||||
|
||||
19
test/client/unit/specs/store/getters/tasks/canDelete.js
Normal file
19
test/client/unit/specs/store/getters/tasks/canDelete.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
141
test/client/unit/specs/store/getters/tasks/getTaskClasses.js
Normal file
141
test/client/unit/specs/store/getters/tasks/getTaskClasses.js
Normal file
@@ -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
test/common/libs/hasClass.test.js
Normal file
52
test/common/libs/hasClass.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
21
test/common/ops/armoireCanOwn.js
Normal file
21
test/common/ops/armoireCanOwn.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import armoireSet from '../../../website/common/script/content/gear/sets/armoire';
|
||||
|
||||
describe('armoireSet items', () => {
|
||||
it('checks if canOwn has the same id', () => {
|
||||
for (const type of Object.keys(armoireSet)) {
|
||||
for (const itemKey of Object.keys(armoireSet[type])) {
|
||||
const ownedKey = `${type}_armoire_${itemKey}`;
|
||||
|
||||
expect(armoireSet[type][itemKey].canOwn({
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
[ownedKey]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}), `${ownedKey} canOwn is broken`).to.eq(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -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
test/common/ops/spells.js
Normal file
38
test/common/ops/spells.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import spells from '../../../website/common/script/content/spells';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
|
||||
// TODO complete the test suite...
|
||||
|
||||
describe('shared.ops.spells', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when healer tries to cast Healing Light with full health', (done) => {
|
||||
user.stats.class = 'healer';
|
||||
user.stats.lvl = 11;
|
||||
user.stats.hp = 50;
|
||||
user.stats.mp = 200;
|
||||
|
||||
let spell = spells.healer.heal;
|
||||
|
||||
try {
|
||||
spell.cast(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
|
||||
expect(user.stats.hp).to.eql(50);
|
||||
expect(user.stats.mp).to.eql(200);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,96 +1,102 @@
|
||||
.promo_aquatic_glass_potions {
|
||||
.achievement-costumeContest6x {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -853px 0px;
|
||||
background-position: -1013px -798px;
|
||||
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: -1013px -208px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_armoire_backgrounds_201807 {
|
||||
.promo_armoire_backgrounds_201810 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -995px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
background-position: -848px -1009px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_fall_avatar_customizations {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1013px 0px;
|
||||
width: 336px;
|
||||
height: 207px;
|
||||
}
|
||||
.promo_fall_festival_2017 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -481px -453px;
|
||||
width: 414px;
|
||||
height: 210px;
|
||||
}
|
||||
.promo_fall_festival_2018 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -367px -723px;
|
||||
width: 393px;
|
||||
height: 213px;
|
||||
}
|
||||
.promo_forest_friends_bundle {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -1009px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_ghost_potions {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -1009px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_ios {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -477px 0px;
|
||||
background-position: 0px -361px;
|
||||
width: 375px;
|
||||
height: 361px;
|
||||
}
|
||||
.promo_mystery_201807 {
|
||||
.promo_mystery_201809 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -995px -442px;
|
||||
width: 114px;
|
||||
height: 120px;
|
||||
}
|
||||
.promo_naming_day_2018 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -518px -365px;
|
||||
width: 285px;
|
||||
height: 162px;
|
||||
}
|
||||
.promo_orcas {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -954px;
|
||||
width: 219px;
|
||||
background-position: -1013px -650px;
|
||||
width: 306px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_seafoam {
|
||||
.promo_seasonal_shop {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -853px -442px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_seaserpent {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 476px;
|
||||
height: 364px;
|
||||
}
|
||||
.promo_seasonal_shop_summer {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -142px -811px;
|
||||
background-position: -1155px -503px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_splashy_skins {
|
||||
.promo_spooky_sparkles {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -142px -365px;
|
||||
width: 375px;
|
||||
height: 186px;
|
||||
}
|
||||
.customize-option.promo_splashy_skins {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -167px -380px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_summer_splash_2018 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -365px;
|
||||
width: 141px;
|
||||
height: 588px;
|
||||
background-position: -1155px -208px;
|
||||
width: 140px;
|
||||
height: 294px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -995px -563px;
|
||||
background-position: -1158px -798px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
.scene_nametag {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -642px -552px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-position: -481px -244px;
|
||||
width: 512px;
|
||||
height: 208px;
|
||||
}
|
||||
.scene_pomodoro {
|
||||
.scene_positivity {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -142px -552px;
|
||||
width: 258px;
|
||||
height: 258px;
|
||||
background-position: -481px 0px;
|
||||
width: 531px;
|
||||
height: 243px;
|
||||
}
|
||||
.scene_todos {
|
||||
.scene_tools {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -401px -552px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
background-position: 0px -723px;
|
||||
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
474
website/client/assets/css/sprites/spritesmith-main-23.css
Normal file
474
website/client/assets/css/sprites/spritesmith-main-23.css
Normal file
@@ -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
BIN
website/client/assets/images/npc/fall/npc_bailey.png
Normal file
BIN
website/client/assets/images/npc/fall/npc_bailey.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
website/raw_sprites/spritesmith/npcs/npc_vicky.png → website/client/assets/images/npc/fall/npc_justin.png
Executable file → Normal file
BIN
website/raw_sprites/spritesmith/npcs/npc_vicky.png → website/client/assets/images/npc/fall/npc_justin.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
website/client/assets/images/npc/fall/npc_matt.png
Normal file
BIN
website/client/assets/images/npc/fall/npc_matt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user