diff --git a/common/script/index.js b/common/script/index.js index 9d2245e662..9d33f32acb 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -109,6 +109,8 @@ import buyMysterySet from './ops/buyMysterySet'; import buyQuest from './ops/buyQuest'; import buySpecialSpell from './ops/buySpecialSpell'; import allocateNow from './ops/allocateNow'; +import changeClass from './ops/changeClass'; +import disableClasses from './ops/disableClasses'; api.ops = { scoreTask, @@ -119,6 +121,8 @@ api.ops = { buySpecialSpell, buyQuest, allocateNow, + changeClass, + disableClasses, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/changeClass.js b/common/script/ops/changeClass.js index 98fd8fd58d..83452709c3 100644 --- a/common/script/ops/changeClass.js +++ b/common/script/ops/changeClass.js @@ -2,58 +2,70 @@ import i18n from '../i18n'; import _ from 'lodash'; import splitWhitespace from '../libs/splitWhitespace'; import { capByLevel } from '../statHelpers'; +import { + NotAuthorized, +} from '../libs/errors'; + +module.exports = function changeClass (user, req = {}, analytics) { + let klass = _.get(req, 'query.class'); -module.exports = function(user, req, cb, analytics) { - var analyticsData, klass, ref; - klass = (ref = req.query) != null ? ref["class"] : void 0; if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') { - analyticsData = { - uuid: user._id, - "class": klass, - acquireMethod: 'Gems', - gemCost: 3, - category: 'behavior' - }; - if (analytics != null) { - analytics.track('change class', analyticsData); - } - user.stats["class"] = klass; + user.stats.class = klass; user.flags.classSelected = true; - _.each(["weapon", "armor", "shield", "head"], function(type) { - var foundKey; - foundKey = false; - _.findLast(user.items.gear.owned, function(v, k) { - if (~k.indexOf(type + "_" + klass) && v === true) { - return foundKey = k; + + _.each(['weapon', 'armor', 'shield', 'head'], (type) => { + let foundKey = false; + _.findLast(user.items.gear.owned, (val, key) => { + if (key.indexOf(`${type}_${klass}`) !== -1 && val === true) { + foundKey = key; + return true; } }); - user.items.gear.equipped[type] = foundKey ? foundKey : type === "weapon" ? "weapon_" + klass + "_0" : type === "shield" && klass === "rogue" ? "shield_rogue_0" : type + "_base_0"; - if (type === "weapon" || (type === "shield" && klass === "rogue")) { - user.items.gear.owned[type + "_" + klass + "_0"] = true; + + if (!foundKey) { + if (type === 'weapon') { + foundKey = `weapon_${klass}_0`; + } else if (type === 'shield' && klass === 'rogue') { + foundKey = 'shield_rogue_0'; + } else { + foundKey = `${type}_base_0`; + } + } + + user.items.gear.equipped[type] = foundKey; + + if (type === 'weapon' || (type === 'shield' && klass === 'rogue')) { // eslint-disable-line no-extra-parens + user.items.gear.owned[`${type}_${klass}_0`] = true; } - return true; }); + + if (analytics) { + analytics.track('change class', { + uuid: user._id, + class: klass, + acquireMethod: 'Gems', + gemCost: 3, + category: 'behavior', + }); + } } else { if (user.preferences.disableClasses) { user.preferences.disableClasses = false; user.preferences.autoAllocate = false; } else { - if (!(user.balance >= .75)) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('notEnoughGems', req.language) - }) : void 0; - } - user.balance -= .75; + if (user.balance < 0.75) throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); + user.balance -= 0.75; } - _.merge(user.stats, { - str: 0, - con: 0, - per: 0, - int: 0, - points: capByLevel(user.stats.lvl) - }); + + user.stats.str = 0; + user.stats.con = 0; + user.stats.per = 0; + user.stats.int = 0; + user.stats.points = capByLevel(user.stats.lvl); user.flags.classSelected = false; } - return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats flags items preferences'))) : void 0; + + return { + data: _.pick(user, splitWhitespace('stats flags items preferences')), + }; }; diff --git a/common/script/ops/disableClasses.js b/common/script/ops/disableClasses.js index 89636ed52d..0aae65edd5 100644 --- a/common/script/ops/disableClasses.js +++ b/common/script/ops/disableClasses.js @@ -2,12 +2,15 @@ import splitWhitespace from '../libs/splitWhitespace'; import { capByLevel } from '../statHelpers'; import _ from 'lodash'; -module.exports = function(user, req, cb) { - user.stats["class"] = 'warrior'; +module.exports = function disableClasses (user) { + user.stats.class = 'warrior'; user.flags.classSelected = true; user.preferences.disableClasses = true; user.preferences.autoAllocate = true; user.stats.str = capByLevel(user.stats.lvl); user.stats.points = 0; - return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats flags preferences'))) : void 0; + + return { + data: _.pick(user, splitWhitespace('stats flags preferences')), + }; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 25a0f6a978..e6a1380d09 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -20,14 +20,12 @@ const COMMON_FILES = [ '!./common/script/ops/addTask.js', '!./common/script/ops/addWebhook.js', '!./common/script/ops/blockUser.js', - '!./common/script/ops/changeClass.js', '!./common/script/ops/clearCompleted.js', '!./common/script/ops/clearPMs.js', '!./common/script/ops/deletePM.js', '!./common/script/ops/deleteTag.js', '!./common/script/ops/deleteTask.js', '!./common/script/ops/deleteWebhook.js', - '!./common/script/ops/disableClasses.js', '!./common/script/ops/equip.js', '!./common/script/ops/feed.js', '!./common/script/ops/getTag.js', diff --git a/test/api/v3/integration/user/POST-user_change-class.test.js b/test/api/v3/integration/user/POST-user_change-class.test.js new file mode 100644 index 0000000000..3a636d1c42 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_change-class.test.js @@ -0,0 +1,27 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/change-class', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('changes class', async () => { + let res = await user.post(`/user/change-class?class=rogue`); + await user.sync(); + + expect(res).to.eql({ + data: JSON.parse(JSON.stringify({ + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + items: user.items, + })), + }); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_disable-classes.test.js b/test/api/v3/integration/user/POST-user_disable-classes.test.js new file mode 100644 index 0000000000..79272e83fd --- /dev/null +++ b/test/api/v3/integration/user/POST-user_disable-classes.test.js @@ -0,0 +1,26 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/disable-classes', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('disable classes', async () => { + let res = await user.post(`/user/disable-classes`); + await user.sync(); + + expect(res).to.eql({ + data: JSON.parse(JSON.stringify({ + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + })), + }); + }); +}); diff --git a/test/common/ops/changeClass.js b/test/common/ops/changeClass.js new file mode 100644 index 0000000000..7c903362e7 --- /dev/null +++ b/test/common/ops/changeClass.js @@ -0,0 +1,119 @@ +import changeClass from '../../../common/script/ops/changeClass'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.changeClass', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + context('req.query.class is a valid class', () => { + it('changes class', () => { + user.stats.class = 'healer'; + user.items.gear.owned.armor_rogue_1 = true; // eslint-disable-line camelcase + + let res = changeClass(user, {query: {class: 'rogue'}}); + expect(res).to.eql({ + data: { + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + items: user.items, + }, + }); + + expect(user.stats.class).to.equal('rogue'); + expect(user.flags.classSelected).to.be.true; + expect(user.items.gear.equipped.weapon).to.equal('weapon_rogue_0'); + expect(user.items.gear.owned.weapon_rogue_0).to.be.true; + expect(user.items.gear.equipped.armor).to.equal('armor_rogue_1'); + expect(user.items.gear.owned.armor_rogue_1).to.be.true; + expect(user.items.gear.equipped.shield).to.equal('shield_rogue_0'); + expect(user.items.gear.owned.shield_rogue_0).to.be.true; + expect(user.items.gear.equipped.head).to.equal('head_base_0'); + }); + }); + + context('req.query.class is missing', () => { + it('has user.preferences.disableClasses === true', () => { + user.balance = 1; + user.preferences.disableClasses = true; + user.preferences.autoAllocate = true; + user.stats.points = 45; + user.stats.lvl = 3; + user.stats.str = 1; + user.stats.con = 2; + user.stats.per = 3; + user.stats.int = 4; + user.flags.classSelected = true; + + let res = changeClass(user); + expect(res).to.eql({ + data: { + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + items: user.items, + }, + }); + + expect(user.preferences.disableClasses).to.be.false; + expect(user.preferences.autoAllocate).to.be.false; + expect(user.balance).to.equal(1); + expect(user.stats.str).to.equal(0); + expect(user.stats.con).to.equal(0); + expect(user.stats.per).to.equal(0); + expect(user.stats.int).to.equal(0); + expect(user.stats.points).to.equal(3); + expect(user.flags.classSelected).to.equal(false); + }); + + context('has user.preferences.disableClasses !== true', () => { + it('and less than 3 gems', () => { + user.balance = 0.5; + try { + changeClass(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('notEnoughGems')); + } + }); + + it('and at least 3 gems', () => { + user.balance = 1; + user.stats.points = 45; + user.stats.lvl = 3; + user.stats.str = 1; + user.stats.con = 2; + user.stats.per = 3; + user.stats.int = 4; + user.flags.classSelected = true; + + let res = changeClass(user); + expect(res).to.eql({ + data: { + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + items: user.items, + }, + }); + + expect(user.balance).to.equal(0.25); + expect(user.stats.str).to.equal(0); + expect(user.stats.con).to.equal(0); + expect(user.stats.per).to.equal(0); + expect(user.stats.int).to.equal(0); + expect(user.stats.points).to.equal(3); + expect(user.flags.classSelected).to.equal(false); + }); + }); + }); +}); diff --git a/test/common/ops/disableClasses.js b/test/common/ops/disableClasses.js new file mode 100644 index 0000000000..59f0643c65 --- /dev/null +++ b/test/common/ops/disableClasses.js @@ -0,0 +1,37 @@ +import disableClasses from '../../../common/script/ops/disableClasses'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.disableClasses', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + it('disable classes', () => { + user.stats.lvl = 34; + user.stats.str = 45; + user.stats.class = 'healer'; + user.preferences.disableClasses = false; + user.preferences.autoAllocate = false; + user.stats.points = 2; + + let res = disableClasses(user); + expect(res).to.eql({ + data: { + preferences: user.preferences, + stats: user.stats, + flags: user.flags, + }, + }); + + expect(user.stats.class).to.equal('warrior'); + expect(user.flags.classSelected).to.equal(true); + expect(user.preferences.disableClasses).to.equal(true); + expect(user.preferences.autoAllocate).to.equal(true); + expect(user.stats.str).to.equal(34); + expect(user.stats.points).to.equal(0); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 11bd137fa5..5f828a033c 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -309,4 +309,46 @@ api.buySpecialSpell = { }, }; +/** + * @api {post} /user/change-class Change class. + * @apiVersion 3.0.0 + * @apiName UserChangeClass + * @apiGroup User + * + * @apiParam {string} class ?class={warrior|rogue|wizard|healer}. If missing will + * + * @apiSuccess {Object} data `stats flags items preferences` + */ +api.changeClass = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/change-class', + async handler (req, res) { + let user = res.locals.user; + let changeClassRes = common.ops.changeClass(user, req, res.analytics); + await user.save(); + res.respond(200, changeClassRes); + }, +}; + +/** + * @api {post} /user/disable-classes Disable classes. + * @apiVersion 3.0.0 + * @apiName UserDisableClasses + * @apiGroup User + * + * @apiSuccess {Object} data `stats flags preferences` + */ +api.disableClasses = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/disable-classes', + async handler (req, res) { + let user = res.locals.user; + let disableClassesRes = common.ops.disableClasses(user, req); + await user.save(); + res.respond(200, disableClassesRes); + }, +}; + module.exports = api;