mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
add tests, move CustomError to common, fix casting
This commit is contained in:
@@ -73,7 +73,7 @@
|
|||||||
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
|
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
|
||||||
"questNotFound": "Quest \"<%= key %>\" not found.",
|
"questNotFound": "Quest \"<%= key %>\" not found.",
|
||||||
"questNotOwned": "You don't own that quest scroll.",
|
"questNotOwned": "You don't own that quest scroll.",
|
||||||
"questLevelTooHigh": "You must be Level <%= level %> to begin this quest.",
|
"questLevelTooHigh": "You must be level <%= level %> to begin this quest.",
|
||||||
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
|
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
|
||||||
"questAlreadyAccepted": "You already accepted the quest invitation.",
|
"questAlreadyAccepted": "You already accepted the quest invitation.",
|
||||||
"noActiveQuestToLeave": "No active quest to leave",
|
"noActiveQuestToLeave": "No active quest to leave",
|
||||||
@@ -90,5 +90,11 @@
|
|||||||
"noAdminAccess": "You don't have admin access.",
|
"noAdminAccess": "You don't have admin access.",
|
||||||
"pageMustBeNumber": "req.query.page must be a number",
|
"pageMustBeNumber": "req.query.page must be a number",
|
||||||
"missingUnsubscriptionCode": "Missing unsubscription code.",
|
"missingUnsubscriptionCode": "Missing unsubscription code.",
|
||||||
"userNotFound": "User not Found"
|
"userNotFound": "User not found.",
|
||||||
|
"spellNotFound": "Spell \"<%= spellId %>\" not found.",
|
||||||
|
"partyNotFound": "Party not found",
|
||||||
|
"targetIdUUID": "\"targetId\" must be a valid UUID.",
|
||||||
|
"challengeTasksNoCast": "Casting a spell on challenge tasks is not supported.",
|
||||||
|
"spellNotOwned": "You don't own this spell.",
|
||||||
|
"spellLevelTooHigh": "You must be level <%= level %> to use this spell."
|
||||||
}
|
}
|
||||||
|
|||||||
8
common/script/api-v3/customError.js
Normal file
8
common/script/api-v3/customError.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Base class for custom application errors
|
||||||
|
// It extends Error and capture the stack trace
|
||||||
|
export default class CustomError extends Error {
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
common/script/api-v3/errors.js
Normal file
9
common/script/api-v3/errors.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import CustomError from './customError';
|
||||||
|
|
||||||
|
export class NotAuthorized extends CustomError {
|
||||||
|
constructor (customMessage) {
|
||||||
|
super();
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.message = customMessage || 'Not authorized.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import t from './translation';
|
import t from './translation';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { NotAuthorized } from '../api-v3/errors';
|
||||||
/*
|
/*
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
Spells
|
Spells
|
||||||
@@ -40,14 +40,12 @@ spells.wizard = {
|
|||||||
lvl: 11,
|
lvl: 11,
|
||||||
target: 'task',
|
target: 'task',
|
||||||
notes: t('spellWizardFireballNotes'),
|
notes: t('spellWizardFireballNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
let bonus = user._statsComputed.int * user.fns.crit('per');
|
let bonus = user._statsComputed.int * user.fns.crit('per');
|
||||||
bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * 0.075);
|
bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * 0.075);
|
||||||
user.stats.exp += diminishingReturns(bonus, 75);
|
user.stats.exp += diminishingReturns(bonus, 75);
|
||||||
if (!user.party.quest.progress) user.party.quest.progress = 0;
|
if (!user.party.quest.progress) user.party.quest.progress = 0;
|
||||||
user.party.quest.progress.up += Math.ceil(user._statsComputed.int * 9.1);
|
user.party.quest.progress.up += Math.ceil(user._statsComputed.int * 9.1);
|
||||||
// TODO change, pass req to spell?
|
|
||||||
let req = {language: user.preferences.language};
|
|
||||||
user.fns.updateStats(user.stats, req);
|
user.fns.updateStats(user.stats, req);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -166,12 +164,11 @@ spells.rogue = {
|
|||||||
lvl: 12,
|
lvl: 12,
|
||||||
target: 'task',
|
target: 'task',
|
||||||
notes: t('spellRogueBackStabNotes'),
|
notes: t('spellRogueBackStabNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
let _crit = user.fns.crit('str', 0.3);
|
let _crit = user.fns.crit('str', 0.3);
|
||||||
let bonus = calculateBonus(target.value, user._statsComputed.str, _crit);
|
let bonus = calculateBonus(target.value, user._statsComputed.str, _crit);
|
||||||
user.stats.exp += diminishingReturns(bonus, 75, 50);
|
user.stats.exp += diminishingReturns(bonus, 75, 50);
|
||||||
user.stats.gp += diminishingReturns(bonus, 18, 75);
|
user.stats.gp += diminishingReturns(bonus, 18, 75);
|
||||||
let req = {language: user.preferences.language};
|
|
||||||
user.fns.updateStats(user.stats, req);
|
user.fns.updateStats(user.stats, req);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -262,9 +259,11 @@ spells.special = {
|
|||||||
text: t('spellSpecialSnowballAuraText'),
|
text: t('spellSpecialSnowballAuraText'),
|
||||||
mana: 0,
|
mana: 0,
|
||||||
value: 15,
|
value: 15,
|
||||||
|
previousPurchase: true,
|
||||||
target: 'user',
|
target: 'user',
|
||||||
notes: t('spellSpecialSnowballAuraNotes'),
|
notes: t('spellSpecialSnowballAuraNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
|
if (!user.items.special.snowball) throw new NotAuthorized(t('spellNotOwned')(req.language));
|
||||||
target.stats.buffs.snowball = true;
|
target.stats.buffs.snowball = true;
|
||||||
target.stats.buffs.spookDust = false;
|
target.stats.buffs.spookDust = false;
|
||||||
target.stats.buffs.shinySeed = false;
|
target.stats.buffs.shinySeed = false;
|
||||||
@@ -290,9 +289,11 @@ spells.special = {
|
|||||||
text: t('spellSpecialSpookDustText'),
|
text: t('spellSpecialSpookDustText'),
|
||||||
mana: 0,
|
mana: 0,
|
||||||
value: 15,
|
value: 15,
|
||||||
|
previousPurchase: true,
|
||||||
target: 'user',
|
target: 'user',
|
||||||
notes: t('spellSpecialSpookDustNotes'),
|
notes: t('spellSpecialSpookDustNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
|
if (!user.items.special.spookDust) throw new NotAuthorized(t('spellNotOwned')(req.language));
|
||||||
target.stats.buffs.snowball = false;
|
target.stats.buffs.snowball = false;
|
||||||
target.stats.buffs.spookDust = true;
|
target.stats.buffs.spookDust = true;
|
||||||
target.stats.buffs.shinySeed = false;
|
target.stats.buffs.shinySeed = false;
|
||||||
@@ -318,9 +319,11 @@ spells.special = {
|
|||||||
text: t('spellSpecialShinySeedText'),
|
text: t('spellSpecialShinySeedText'),
|
||||||
mana: 0,
|
mana: 0,
|
||||||
value: 15,
|
value: 15,
|
||||||
|
previousPurchase: true,
|
||||||
target: 'user',
|
target: 'user',
|
||||||
notes: t('spellSpecialShinySeedNotes'),
|
notes: t('spellSpecialShinySeedNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
|
if (!user.items.special.shinySeed) throw new NotAuthorized(t('spellNotOwned')(req.language));
|
||||||
target.stats.buffs.snowball = false;
|
target.stats.buffs.snowball = false;
|
||||||
target.stats.buffs.spookDust = false;
|
target.stats.buffs.spookDust = false;
|
||||||
target.stats.buffs.shinySeed = true;
|
target.stats.buffs.shinySeed = true;
|
||||||
@@ -346,9 +349,11 @@ spells.special = {
|
|||||||
text: t('spellSpecialSeafoamText'),
|
text: t('spellSpecialSeafoamText'),
|
||||||
mana: 0,
|
mana: 0,
|
||||||
value: 15,
|
value: 15,
|
||||||
|
previousPurchase: true,
|
||||||
target: 'user',
|
target: 'user',
|
||||||
notes: t('spellSpecialSeafoamNotes'),
|
notes: t('spellSpecialSeafoamNotes'),
|
||||||
cast (user, target) {
|
cast (user, target, req) {
|
||||||
|
if (!user.items.special.seafoam) throw new NotAuthorized(t('spellNotOwned')(req.language));
|
||||||
target.stats.buffs.snowball = false;
|
target.stats.buffs.snowball = false;
|
||||||
target.stats.buffs.spookDust = false;
|
target.stats.buffs.spookDust = false;
|
||||||
target.stats.buffs.shinySeed = false;
|
target.stats.buffs.shinySeed = false;
|
||||||
@@ -499,8 +504,8 @@ _.each(spells, (spellClass) => {
|
|||||||
_.each(spellClass, (spell, key) => {
|
_.each(spellClass, (spell, key) => {
|
||||||
spell.key = key;
|
spell.key = key;
|
||||||
let _cast = spell.cast;
|
let _cast = spell.cast;
|
||||||
spell.cast = function castSpell (user, target) {
|
spell.cast = function castSpell (user, target, req) {
|
||||||
_cast(user, target);
|
_cast(user, target, req);
|
||||||
user.stats.mp -= spell.mana;
|
user.stats.mp -= spell.mana;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
createAndPopulateGroup,
|
||||||
|
generateChallenge,
|
||||||
|
sleep,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
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: t('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: t('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: 400,
|
||||||
|
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(`/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 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);
|
||||||
|
await group.sync();
|
||||||
|
expect(group.chat[0]).to.exists;
|
||||||
|
expect(group.chat[0].uuid).to.equal('system');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO find a way to have sinon working in integration tests
|
||||||
|
// it doesn't work when tests are running separately from server
|
||||||
|
it('passes correct target to spell when targetType === \'task\'');
|
||||||
|
it('passes correct target to spell when targetType === \'tasks\'');
|
||||||
|
it('passes correct target to spell when targetType === \'self\'');
|
||||||
|
it('passes correct target to spell when targetType === \'party\'');
|
||||||
|
it('passes correct target to spell when targetType === \'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');
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import responseMiddleware from '../../../../../website/src/middlewares/api-v3/re
|
|||||||
import getUserLanguage from '../../../../../website/src/middlewares/api-v3/getUserLanguage';
|
import getUserLanguage from '../../../../../website/src/middlewares/api-v3/getUserLanguage';
|
||||||
|
|
||||||
import { BadRequest } from '../../../../../website/src/libs/api-v3/errors';
|
import { BadRequest } from '../../../../../website/src/libs/api-v3/errors';
|
||||||
|
import { NotAuthorized as NotAuthorizedShared } from '../../../../../common/script/api-v3/errors';
|
||||||
import logger from '../../../../../website/src/libs/api-v3/logger';
|
import logger from '../../../../../website/src/libs/api-v3/logger';
|
||||||
|
|
||||||
describe('errorHandler', () => {
|
describe('errorHandler', () => {
|
||||||
@@ -86,6 +87,21 @@ describe('errorHandler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handle CustomError(s) from shared code', () => {
|
||||||
|
let error = new NotAuthorizedShared();
|
||||||
|
|
||||||
|
errorHandler(error, req, res, next);
|
||||||
|
|
||||||
|
expect(res.status).to.be.calledOnce;
|
||||||
|
expect(res.json).to.be.calledOnce;
|
||||||
|
|
||||||
|
expect(res.status).to.be.calledWith(400);
|
||||||
|
expect(res.json).to.be.calledWith({
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: 'Not authorized.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('handle http-errors errors', () => {
|
it('handle http-errors errors', () => {
|
||||||
let error = new Error('custom message');
|
let error = new Error('custom message');
|
||||||
error.statusCode = 422;
|
error.statusCode = 422;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import common from '../../../../common';
|
|||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
@@ -43,7 +44,7 @@ api.getUser = {
|
|||||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /user/class/cast/:spell Cast a spell on a target.
|
* @api {post} /user/class/cast/:spellId Cast a spell on a target.
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName UserCast
|
* @apiName UserCast
|
||||||
* @apiGroup User
|
* @apiGroup User
|
||||||
@@ -56,7 +57,7 @@ const partyMembersFields = 'profile.name stats achievements items.special';
|
|||||||
api.castSpell = {
|
api.castSpell = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders(), cron],
|
middlewares: [authWithHeaders(), cron],
|
||||||
url: '/user/class/cast/:spell',
|
url: '/user/class/cast/:spellId',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let spellId = req.params.spellId;
|
let spellId = req.params.spellId;
|
||||||
@@ -71,14 +72,16 @@ api.castSpell = {
|
|||||||
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
|
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
|
||||||
let spell = common.content.spells[klass][spellId];
|
let spell = common.content.spells[klass][spellId];
|
||||||
|
|
||||||
if (!spell) throw new NotFound(res.t('spellNotFound', {spell: spellId}));
|
if (!spell) throw new NotFound(res.t('spellNotFound', {spellId}));
|
||||||
if (spell.mana > user.stats.mp) throw new BadRequest(res.t('notEnoughMana'));
|
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
|
||||||
|
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
|
||||||
|
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));
|
||||||
|
|
||||||
let targetType = spell.target;
|
let targetType = spell.target;
|
||||||
|
|
||||||
if (targetType === 'task') {
|
if (targetType === 'task') {
|
||||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||||
|
|
||||||
// TODO what about challenge tasks? should casting be disabled on them?
|
|
||||||
let task = await Tasks.Task.findOne({
|
let task = await Tasks.Task.findOne({
|
||||||
_id: targetId,
|
_id: targetId,
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
@@ -86,11 +89,11 @@ api.castSpell = {
|
|||||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||||
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
|
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
|
||||||
|
|
||||||
spell.cast(user, task);
|
spell.cast(user, task, req);
|
||||||
await task.save();
|
await task.save();
|
||||||
res.respond(200, task);
|
res.respond(200, task);
|
||||||
} else if (targetType === 'self') {
|
} else if (targetType === 'self') {
|
||||||
spell.cast(user);
|
spell.cast(user, null, req);
|
||||||
await user.save();
|
await user.save();
|
||||||
res.respond(200, user);
|
res.respond(200, user);
|
||||||
} else if (targetType === 'tasks') { // new target type when all the user's tasks are necessary
|
} else if (targetType === 'tasks') { // new target type when all the user's tasks are necessary
|
||||||
@@ -103,7 +106,7 @@ api.castSpell = {
|
|||||||
],
|
],
|
||||||
}).exec();
|
}).exec();
|
||||||
|
|
||||||
spell.cast(user, tasks);
|
spell.cast(user, tasks, req);
|
||||||
|
|
||||||
let toSave = tasks.filter(t => t.isModified());
|
let toSave = tasks.filter(t => t.isModified());
|
||||||
let isUserModified = user.isModified();
|
let isUserModified = user.isModified();
|
||||||
@@ -116,8 +119,7 @@ api.castSpell = {
|
|||||||
if (isUserModified) res.user = user;
|
if (isUserModified) res.user = user;
|
||||||
res.respond(200, response);
|
res.respond(200, response);
|
||||||
} else if (targetType === 'party' || targetType === 'user') {
|
} else if (targetType === 'party' || targetType === 'user') {
|
||||||
let party = await Group.getGroup({_id: 'party', user});
|
let party = await Group.getGroup({groupId: 'party', user});
|
||||||
|
|
||||||
// arrays of users when targetType is 'party' otherwise single users
|
// arrays of users when targetType is 'party' otherwise single users
|
||||||
let partyMembers;
|
let partyMembers;
|
||||||
|
|
||||||
@@ -128,18 +130,19 @@ api.castSpell = {
|
|||||||
partyMembers = await User.find({'party._id': party._id}).select(partyMembersFields).exec();
|
partyMembers = await User.find({'party._id': party._id}).select(partyMembersFields).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
spell.cast(user, partyMembers);
|
spell.cast(user, partyMembers, req);
|
||||||
await Q.all(partyMembers.map(m => m.save()));
|
await Q.all(partyMembers.map(m => m.save()));
|
||||||
} else {
|
} else {
|
||||||
if (!party && (!targetId || user._id === targetId)) {
|
if (!party && (!targetId || user._id === targetId)) {
|
||||||
partyMembers = user;
|
partyMembers = user;
|
||||||
} else {
|
} else {
|
||||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||||
|
if (!party) throw new NotFound(res.t('partyNotFound'));
|
||||||
partyMembers = await User.findOne({_id: targetId, 'party._id': party._id}).select(partyMembersFields).exec();
|
partyMembers = await User.findOne({_id: targetId, 'party._id': party._id}).select(partyMembersFields).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId}));
|
if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId}));
|
||||||
spell.cast(user, partyMembers);
|
spell.cast(user, partyMembers, req);
|
||||||
await partyMembers.save();
|
await partyMembers.save();
|
||||||
}
|
}
|
||||||
res.respond(200, partyMembers);
|
res.respond(200, partyMembers);
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
// Base class for custom application errors
|
import CustomError from '../../../../common/script/api-v3/customError';
|
||||||
// It extends Error and capture the stack trace
|
|
||||||
export class CustomError extends Error {
|
|
||||||
constructor () {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine NotAuthorized
|
* @apiDefine NotAuthorized
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// The error handler middleware that handles all errors
|
// The error handler middleware that handles all errors
|
||||||
// and respond to the client
|
// and respond to the client
|
||||||
import logger from '../../libs/api-v3/logger';
|
import logger from '../../libs/api-v3/logger';
|
||||||
|
import CustomError from '../../../../common/script/api-v3/customError';
|
||||||
import {
|
import {
|
||||||
CustomError,
|
|
||||||
BadRequest,
|
BadRequest,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
@@ -24,11 +24,17 @@ export default function errorHandler (err, req, res, next) { // eslint-disable-l
|
|||||||
// If we can't identify it, respond with a generic 500 error
|
// If we can't identify it, respond with a generic 500 error
|
||||||
let responseErr = err instanceof CustomError ? err : null;
|
let responseErr = err instanceof CustomError ? err : null;
|
||||||
|
|
||||||
|
// TODO don't return always 400 for errors in common code
|
||||||
|
// If CustomError but without httpCode then they come from shared code, treat as 400s
|
||||||
|
if (err instanceof CustomError && !err.httpCode) {
|
||||||
|
err.httpCode = 400;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle errors created with 'http-errors' or similar that have a status/statusCode property
|
// Handle errors created with 'http-errors' or similar that have a status/statusCode property
|
||||||
if (err.statusCode && typeof err.statusCode === 'number') {
|
if (err.statusCode && typeof err.statusCode === 'number') {
|
||||||
responseErr = new CustomError();
|
responseErr = new CustomError();
|
||||||
responseErr.httpCode = err.statusCode;
|
responseErr.httpCode = err.statusCode;
|
||||||
responseErr.error = err.name;
|
responseErr.name = err.name;
|
||||||
responseErr.message = err.message;
|
responseErr.message = err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,20 +125,20 @@ schema.statics.getGroup = async function getGroup (options = {}) {
|
|||||||
let {user, groupId, fields, optionalMembership = false, populateLeader = false, requireMembership = false} = options;
|
let {user, groupId, fields, optionalMembership = false, populateLeader = false, requireMembership = false} = options;
|
||||||
let query;
|
let query;
|
||||||
|
|
||||||
let isParty = groupId === 'party' || user.party._id === groupId;
|
let isUserParty = groupId === 'party' || user.party._id === groupId;
|
||||||
let isGuild = user.guilds.indexOf(groupId) !== -1;
|
let isUserGuild = user.guilds.indexOf(groupId) !== -1;
|
||||||
|
|
||||||
// When requireMembership is true check that user is member even in public guild
|
// When requireMembership is true check that user is member even in public guild
|
||||||
if (requireMembership && !isParty && !isGuild) {
|
if (requireMembership && !isUserParty && !isUserGuild) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When optionalMembership is true it's not required for the user to be a member of the group
|
// When optionalMembership is true it's not required for the user to be a member of the group
|
||||||
if (isParty) {
|
if (isUserParty) {
|
||||||
query = {type: 'party', _id: user.party._id};
|
query = {type: 'party', _id: user.party._id};
|
||||||
} else if (optionalMembership === true) {
|
} else if (optionalMembership === true) {
|
||||||
query = {_id: groupId};
|
query = {_id: groupId};
|
||||||
} else if (isGuild) {
|
} else if (isUserGuild) {
|
||||||
query = {type: 'guild', _id: groupId};
|
query = {type: 'guild', _id: groupId};
|
||||||
} else {
|
} else {
|
||||||
query = {type: 'guild', privacy: 'public', _id: groupId};
|
query = {type: 'guild', privacy: 'public', _id: groupId};
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ export let schema = new Schema({
|
|||||||
lvl: {type: Number, default: 1},
|
lvl: {type: Number, default: 1},
|
||||||
|
|
||||||
// Class System
|
// Class System
|
||||||
class: {type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior'},
|
class: {type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior', required: true},
|
||||||
points: {type: Number, default: 0},
|
points: {type: Number, default: 0},
|
||||||
str: {type: Number, default: 0},
|
str: {type: Number, default: 0},
|
||||||
con: {type: Number, default: 0},
|
con: {type: Number, default: 0},
|
||||||
|
|||||||
Reference in New Issue
Block a user