diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 58b3746bfe..9f48532913 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -100,5 +100,7 @@ "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." + "spellLevelTooHigh": "You must be level <%= level %> to use this spell.", + "invalidAttribute": "\"<%= attr %>\" is not a valid attribute.", + "notEnoughAttrPoints": "You don't have enough attribute points." } diff --git a/common/script/constants.js b/common/script/constants.js index d6e0aa9384..2dc8663091 100644 --- a/common/script/constants.js +++ b/common/script/constants.js @@ -1,3 +1,4 @@ export const MAX_HEALTH = 50; export const MAX_LEVEL = 100; export const MAX_STAT_POINTS = MAX_LEVEL; +export const ATTRIBUTES = ['str', 'int', 'per', 'con']; diff --git a/common/script/index.js b/common/script/index.js index 6d26b32c48..a7bdf6e3d1 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -100,10 +100,12 @@ api.count = count; // TODO As ops and fns are ported, exported them through the api object import scoreTask from './ops/scoreTask'; import sleep from './ops/sleep'; +import allocate from './ops/allocate'; api.ops = { scoreTask, sleep, + allocate, }; api.fns = {}; diff --git a/common/script/ops/allocate.js b/common/script/ops/allocate.js index 92b5ae53fa..c04c756e45 100644 --- a/common/script/ops/allocate.js +++ b/common/script/ops/allocate.js @@ -1,15 +1,30 @@ import _ from 'lodash'; import splitWhitespace from '../libs/splitWhitespace'; +import { + ATTRIBUTES, +} from '../constants'; +import { + BadRequest, + NotAuthorized, +} from '../libs/errors'; +import i18n from '../i18n'; + +module.exports = function allocate (user, req = {}) { + let stat = _.get(req, 'query.stat', 'str'); + + if (ATTRIBUTES.indexOf(stat) === -1) { + throw new BadRequest(i18n.t('invalidAttribute', {attr: stat}, req.language)); + } -module.exports = function(user, req, cb) { - var stat; - stat = req.query.stat || 'str'; if (user.stats.points > 0) { user.stats[stat]++; user.stats.points--; if (stat === 'int') { user.stats.mp++; } + } else { + throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language)); } - return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats'))) : void 0; + + return _.pick(user, splitWhitespace('stats')); }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 6d8c1ca2dd..1c5df3f6f8 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -19,7 +19,6 @@ const COMMON_FILES = [ '!./common/script/ops/addTag.js', '!./common/script/ops/addTask.js', '!./common/script/ops/addWebhook.js', - '!./common/script/ops/allocate.js', '!./common/script/ops/allocateNow.js', '!./common/script/ops/blockUser.js', '!./common/script/ops/buy.js', diff --git a/test/api/v3/integration/user/POST-user_allocate.test.js b/test/api/v3/integration/user/POST-user_allocate.test.js new file mode 100644 index 0000000000..6f3e6347ac --- /dev/null +++ b/test/api/v3/integration/user/POST-user_allocate.test.js @@ -0,0 +1,41 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/allocate', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('returns an error if an invalid attribute is supplied', async () => { + await expect(user.post(`/user/allocate?stat=invalid`)) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidAttribute', {attr: 'invalid'}), + }); + }); + + it('returns an error if the user doesn\'t have attribute points', async () => { + await expect(user.post(`/user/allocate`)) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notEnoughAttrPoints'), + }); + }); + + it('allocates attribute points', async () => { + await user.update({'stats.points': 1}); + let res = await user.post(`/user/allocate?stat=con`); + await user.sync(); + expect(user.stats.con).to.equal(1); + expect(user.stats.points).to.equal(0); + expect(res.stats.con).to.equal(1); + }); +}); diff --git a/test/api/v3/integration/user/sleep.test.js b/test/api/v3/integration/user/POST-user_sleep.test.js similarity index 93% rename from test/api/v3/integration/user/sleep.test.js rename to test/api/v3/integration/user/POST-user_sleep.test.js index 8951d71f20..eb4a38b8bb 100644 --- a/test/api/v3/integration/user/sleep.test.js +++ b/test/api/v3/integration/user/POST-user_sleep.test.js @@ -9,6 +9,8 @@ describe('POST /user/sleep', () => { user = await generateUser(); }); + // More tests in common code unit tests + it('toggles sleep status', async () => { let res = await user.post(`/user/sleep`); expect(res).to.eql({ diff --git a/test/common/constants.js b/test/common/constants.js new file mode 100644 index 0000000000..e05db90660 --- /dev/null +++ b/test/common/constants.js @@ -0,0 +1,11 @@ +import { + ATTRIBUTES, +} from '../../common/script/constants'; + +describe('constants', () => { + describe('ATTRIBUTES', () => { + it('provides a list of attributes', () => { + expect(ATTRIBUTES).to.eql(['str', 'int', 'per', 'con']); + }); + }); +}); diff --git a/test/common/ops/allocate.js b/test/common/ops/allocate.js new file mode 100644 index 0000000000..84669af92a --- /dev/null +++ b/test/common/ops/allocate.js @@ -0,0 +1,59 @@ +import allocate from '../../../common/script/ops/allocate'; +import { + BadRequest, + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.allocate', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + it('throws an error if an invalid attribute is supplied', () => { + try { + expect(allocate(user, { + query: {stat: 'notValid'}, + })).to.throw(BadRequest); + } catch (err) { + expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'notValid'})); + } + }); + + it('throws an error if the user doesn\'t have attribute points', () => { + try { + expect(allocate(user)).to.throw(NotAuthorized); + } catch (err) { + expect(err.message).to.equal(i18n.t('notEnoughAttrPoints')); + } + }); + + it('defaults to the "str" attribute', () => { + expect(user.stats.str).to.equal(0); + user.stats.points = 1; + allocate(user); + expect(user.stats.str).to.equal(1); + }); + + it('allocates attribute points', () => { + expect(user.stats.con).to.equal(0); + user.stats.points = 1; + allocate(user, {query: {stat: 'con'}}); + expect(user.stats.con).to.equal(1); + expect(user.stats.points).to.equal(0); + }); + + it('increases mana when allocating to "int"', () => { + expect(user.stats.int).to.equal(0); + expect(user.stats.mp).to.equal(10); + user.stats.points = 1; + allocate(user, {query: {stat: 'int'}}); + expect(user.stats.int).to.equal(1); + expect(user.stats.mp).to.equal(11); + }); +}); diff --git a/website/src/controllers/api-v3/tasks.js b/website/src/controllers/api-v3/tasks.js index 8fb0d3b5d4..ed64627aff 100644 --- a/website/src/controllers/api-v3/tasks.js +++ b/website/src/controllers/api-v3/tasks.js @@ -16,8 +16,6 @@ import _ from 'lodash'; import moment from 'moment'; import { preenHistory } from '../../libs/api-v3/preening'; -const scoreTask = common.ops.scoreTask; - let api = {}; // challenge must be passed only when a challenge task is being created @@ -401,7 +399,7 @@ api.scoreTask = { task.completed = direction === 'up'; // TODO move into scoreTask } - let delta = scoreTask({task, user, direction}, req); + let delta = common.ops.scoreTask({task, user, direction}, req); // Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results) if (direction === 'up') user.fns.randomDrop({task, delta}, req); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index f1a9ac49ff..17c37aaf84 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -13,8 +13,6 @@ import Q from 'q'; import _ from 'lodash'; import * as passwordUtils from '../../libs/api-v3/password'; -const sleep = common.ops.sleep; - let api = {}; /** @@ -293,10 +291,30 @@ api.sleep = { url: '/user/sleep', async handler (req, res) { let user = res.locals.user; - let sleepRes = sleep(user); + let sleepRes = common.ops.sleep(user); await user.save(); res.respond(200, sleepRes); }, }; +/** + * @api {post} /user/allocate Allocate an attribute point. + * @apiVersion 3.0.0 + * @apiName UserAllocate + * @apiGroup User + * + * @apiSuccess {Object} Returs `user.stats` + */ +api.allocate = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/allocate', + async handler (req, res) { + let user = res.locals.user; + let allocateRes = common.ops.allocate(user, req); + await user.save(); + res.respond(200, allocateRes); + }, +}; + module.exports = api;