From 9e3d8ba4ac29ce11d0f1604d87bb9f60b1e29dc9 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Tue, 19 Apr 2016 09:50:04 -0500 Subject: [PATCH] Separated out buy functions into buyGear, buyArmoire, and buyPotion (#7065) --- common/locales/en/api-v3.json | 6 +- common/script/index.js | 9 + common/script/ops/buy.js | 134 +------- common/script/ops/buyArmoire.js | 116 +++++++ common/script/ops/buyGear.js | 72 +++++ common/script/ops/buyPotion.js | 52 ++++ common/script/ops/index.js | 6 + .../v3/integration/user/POST-user_buy.test.js | 19 +- .../user/POST-user_buy_armoire.test.js | 44 +++ .../user/POST-user_buy_gear.test.js | 34 +++ .../user/POST-user_buy_potion.test.js | 44 +++ test/common/ops/buy.js | 289 ++---------------- test/common/ops/buyArmoire.js | 187 ++++++++++++ test/common/ops/buyGear.js | 132 ++++++++ test/common/ops/buyPotion.js | 65 ++++ website/src/controllers/api-v3/user.js | 77 ++++- 16 files changed, 877 insertions(+), 409 deletions(-) create mode 100644 common/script/ops/buyArmoire.js create mode 100644 common/script/ops/buyGear.js create mode 100644 common/script/ops/buyPotion.js create mode 100644 test/api/v3/integration/user/POST-user_buy_armoire.test.js create mode 100644 test/api/v3/integration/user/POST-user_buy_gear.test.js create mode 100644 test/api/v3/integration/user/POST-user_buy_potion.test.js create mode 100644 test/common/ops/buyArmoire.js create mode 100644 test/common/ops/buyGear.js create mode 100644 test/common/ops/buyPotion.js diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 7af8dc9af0..183b358e2a 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -113,14 +113,13 @@ "missingKeyParam": "\"req.params.key\" is required.", "mysterySetNotFound": "Mystery set not found, or set already owned.", "itemNotFound": "Item \"<%= key %>\" not found.", - "cannoyBuyItem": "You can't buy this item.", + "cannotBuyItem": "You can't buy this item.", "missingTypeKeyEquip": "\"key\" and \"type\" are required parameters.", "missingPetFoodFeed": "\"pet\" and \"food\" are required parameters.", "invalidPetName": "Invalid pet name supplied.", "missingEggHatchingPotionHatch": "\"egg\" and \"hatchingPotion\" are required parameters.", "invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'.", "cannotDeleteActiveAccount": "You have an active subscription, cancel your plan before deleting your account.", - "cannoyBuyItem": "You can't buy this item", "messageRequired": "A message is required.", "toUserIDRequired": "A toUserId is required", "notAuthorizedToSendMessageToThisUser": "Can't send message to this user.", @@ -170,5 +169,6 @@ "pushDeviceAdded": "Push device added successfully", "pushDeviceAlreadyAdded": "The user already has the push device", "resetComplete": "Reset completed", - "lvl10ChangeClass": "To change class you must be at least level 10." + "lvl10ChangeClass": "To change class you must be at least level 10.", + "equipmentAlreadyOwned": "You already own that piece of equipment" } diff --git a/common/script/index.js b/common/script/index.js index 1db3a88bb7..5191ba5802 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -113,6 +113,9 @@ import scoreTask from './ops/scoreTask'; import sleep from './ops/sleep'; import allocate from './ops/allocate'; import buy from './ops/buy'; +import buyGear from './ops/buyGear'; +import buyPotion from './ops/buyPotion'; +import buyArmoire from './ops/buyArmoire'; import buyMysterySet from './ops/buyMysterySet'; import buyQuest from './ops/buyQuest'; import buySpecialSpell from './ops/buySpecialSpell'; @@ -150,6 +153,9 @@ api.ops = { sleep, allocate, buy, + buyGear, + buyPotion, + buyArmoire, buyMysterySet, buySpecialSpell, buyQuest, @@ -266,6 +272,9 @@ api.wrap = function wrapUser (user, main = true) { releaseMounts: _.partial(importedOps.releaseMounts, user), releaseBoth: _.partial(importedOps.releaseBoth, user), buy: _.partial(importedOps.buy, user), + buyPotion: _.partial(importedOps.buyPotion, user), + buyArmoire: _.partial(importedOps.buyArmoire, user), + buyGear: _.partial(importedOps.buyGear, user), buyQuest: _.partial(importedOps.buyQuest, user), buyMysterySet: _.partial(importedOps.buyMysterySet, user), hourglassPurchase: _.partial(importedOps.hourglassPurchase, user), diff --git a/common/script/ops/buy.js b/common/script/ops/buy.js index 268be94b39..d62842acea 100644 --- a/common/script/ops/buy.js +++ b/common/script/ops/buy.js @@ -1,142 +1,24 @@ -import content from '../content/index'; import i18n from '../i18n'; import _ from 'lodash'; -import count from '../count'; -import splitWhitespace from '../libs/splitWhitespace'; import { BadRequest, - NotAuthorized, - NotFound, } from '../libs/errors'; -import predictableRandom from '../fns/predictableRandom'; -import randomVal from '../fns/randomVal'; -import handleTwoHanded from '../fns/handleTwoHanded'; -import ultimateGear from '../fns/ultimateGear'; +import buyPotion from './buyPotion'; +import buyArmoire from './buyArmoire'; +import buyGear from './buyGear'; module.exports = function buy (user, req = {}, analytics) { let key = _.get(req, 'params.key'); if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language)); - let item; + let buyRes; if (key === 'potion') { - item = content.potion; + buyRes = buyPotion(user, req, analytics); } else if (key === 'armoire') { - item = content.armoire; + buyRes = buyArmoire(user, req, analytics); } else { - item = content.gear.flat[key]; - } - if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language)); - - if (user.stats.gp < item.value) { - throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); + buyRes = buyGear(user, req, analytics); } - if (item.canOwn && !item.canOwn(user)) { - throw new NotAuthorized(i18n.t('cannoyBuyItem', req.language)); - } - - let armoireResp; - let armoireResult; - let eligibleEquipment; - let drop; - let message; - - if (item.key === 'potion') { - user.stats.hp += 15; - if (user.stats.hp > 50) { - user.stats.hp = 50; - } - } else if (item.key === 'armoire') { - armoireResult = predictableRandom(user, user.stats.gp); - eligibleEquipment = _.filter(content.gear.flat, (eligible) => { - return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key]; - }); - - if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) { - eligibleEquipment.sort(); - drop = randomVal(user, eligibleEquipment); - - user.items.gear.owned[drop.key] = true; - user.flags.armoireOpened = true; - message = i18n.t('armoireEquipment', { - image: ``, - dropText: drop.text(req.language), - }, req.language); - - if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) { - user.flags.armoireEmpty = true; - } - - armoireResp = { - type: 'gear', - dropKey: drop.key, - dropText: drop.text(req.language), - }; - } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens - drop = randomVal(user, _.where(content.food, { - canDrop: true, - })); - user.items.food[drop.key] = user.items.food[drop.key] || 0; - user.items.food[drop.key] += 1; - - message = i18n.t('armoireFood', { - image: ``, - dropArticle: drop.article, - dropText: drop.text(req.language), - }, req.language); - armoireResp = { - type: 'food', - dropKey: drop.key, - dropArticle: drop.article, - dropText: drop.text(req.language), - }; - } else { - let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10); - user.stats.exp += armoireExp; - message = i18n.t('armoireExp', req.language); - armoireResp = { - type: 'experience', - value: armoireExp, - }; - } - } else { - if (user.preferences.autoEquip) { - user.items.gear.equipped[item.type] = item.key; - message = handleTwoHanded(user, item, undefined, req); - } - user.items.gear.owned[item.key] = true; - - if (item.last) ultimateGear(user); - } - - user.stats.gp -= item.value; - - if (!message) { - message = i18n.t('messageBought', { - itemText: item.text(req.language), - }, req.language); - } - - if (analytics) { - analytics.track('acquire item', { - uuid: user._id, - itemKey: key, - acquireMethod: 'Gold', - goldCost: item.value, - category: 'behavior', - }); - } - - let res = { - data: _.pick(user, splitWhitespace('items achievements stats flags')), - message, - }; - - if (armoireResp) res.armoire = armoireResp; - - if (req.v2 === true) { - return res.data; - } else { - return res; - } + return buyRes; }; diff --git a/common/script/ops/buyArmoire.js b/common/script/ops/buyArmoire.js new file mode 100644 index 0000000000..9b2a8af8e2 --- /dev/null +++ b/common/script/ops/buyArmoire.js @@ -0,0 +1,116 @@ +import content from '../content/index'; +import i18n from '../i18n'; +import _ from 'lodash'; +import count from '../count'; +import splitWhitespace from '../libs/splitWhitespace'; +import { + NotAuthorized, +} from '../libs/errors'; +import predictableRandom from '../fns/predictableRandom'; +import randomVal from '../fns/randomVal'; + +module.exports = function buyArmoire (user, req = {}, analytics) { + let item = content.armoire; + + if (user.stats.gp < item.value) { + throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); + } + + if (item.canOwn && !item.canOwn(user)) { + throw new NotAuthorized(i18n.t('cannotBuyItem', req.language)); + } + + let armoireResp; + let armoireResult; + let eligibleEquipment; + let drop; + let message; + + armoireResult = predictableRandom(user, user.stats.gp); + eligibleEquipment = _.filter(content.gear.flat, (eligible) => { + return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key]; + }); + + if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) { + eligibleEquipment.sort(); + drop = randomVal(user, eligibleEquipment); + + if (user.items.gear.owned[drop.key]) { + throw new NotAuthorized(i18n.t('equipmentAlradyOwned', req.language)); + } + + user.items.gear.owned[drop.key] = true; + user.flags.armoireOpened = true; + message = i18n.t('armoireEquipment', { + image: ``, + dropText: drop.text(req.language), + }, req.language); + + if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) { + user.flags.armoireEmpty = true; + } + + armoireResp = { + type: 'gear', + dropKey: drop.key, + dropText: drop.text(req.language), + }; + } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens + drop = randomVal(user, _.where(content.food, { + canDrop: true, + })); + user.items.food[drop.key] = user.items.food[drop.key] || 0; + user.items.food[drop.key] += 1; + + message = i18n.t('armoireFood', { + image: ``, + dropArticle: drop.article, + dropText: drop.text(req.language), + }, req.language); + armoireResp = { + type: 'food', + dropKey: drop.key, + dropArticle: drop.article, + dropText: drop.text(req.language), + }; + } else { + let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10); + user.stats.exp += armoireExp; + message = i18n.t('armoireExp', req.language); + armoireResp = { + type: 'experience', + value: armoireExp, + }; + } + + user.stats.gp -= item.value; + + if (!message) { + message = i18n.t('messageBought', { + itemText: item.text(req.language), + }, req.language); + } + + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: 'Armoire', + acquireMethod: 'Gold', + goldCost: item.value, + category: 'behavior', + }); + } + + let res = { + data: _.pick(user, splitWhitespace('items flags')), + message, + }; + + if (armoireResp) res.armoire = armoireResp; + + if (req.v2 === true) { + return res.data; + } else { + return res; + } +}; diff --git a/common/script/ops/buyGear.js b/common/script/ops/buyGear.js new file mode 100644 index 0000000000..2bad8135cd --- /dev/null +++ b/common/script/ops/buyGear.js @@ -0,0 +1,72 @@ +import content from '../content/index'; +import i18n from '../i18n'; +import _ from 'lodash'; +import splitWhitespace from '../libs/splitWhitespace'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../libs/errors'; +import handleTwoHanded from '../fns/handleTwoHanded'; +import ultimateGear from '../fns/ultimateGear'; + +module.exports = function buyGear (user, req = {}, analytics) { + let key = _.get(req, 'params.key'); + if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language)); + + let item = content.gear.flat[key]; + + if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language)); + + if (user.stats.gp < item.value) { + throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); + } + + if (item.canOwn && !item.canOwn(user)) { + throw new NotAuthorized(i18n.t('cannotBuyItem', req.language)); + } + + let message; + + if (user.items.gear.owned[item.key]) { + throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language)); + } + + if (user.preferences.autoEquip) { + user.items.gear.equipped[item.type] = item.key; + message = handleTwoHanded(user, item, undefined, req); + } + + user.items.gear.owned[item.key] = true; + + if (item.last) ultimateGear(user); + + user.stats.gp -= item.value; + + if (!message) { + message = i18n.t('messageBought', { + itemText: item.text(req.language), + }, req.language); + } + + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: key, + acquireMethod: 'Gold', + goldCost: item.value, + category: 'behavior', + }); + } + + let res = { + data: _.pick(user, splitWhitespace('items achievements stats flags')), + message, + }; + + if (req.v2 === true) { + return res.data; + } else { + return res; + } +}; diff --git a/common/script/ops/buyPotion.js b/common/script/ops/buyPotion.js new file mode 100644 index 0000000000..f797ff903b --- /dev/null +++ b/common/script/ops/buyPotion.js @@ -0,0 +1,52 @@ +import content from '../content/index'; +import i18n from '../i18n'; +import _ from 'lodash'; +import splitWhitespace from '../libs/splitWhitespace'; +import { + NotAuthorized, +} from '../libs/errors'; + +module.exports = function buyPotion (user, req = {}, analytics) { + let item = content.potion; + + if (user.stats.gp < item.value) { + throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); + } + + if (item.canOwn && !item.canOwn(user)) { + throw new NotAuthorized(i18n.t('cannotBuyItem', req.language)); + } + + user.stats.hp += 15; + if (user.stats.hp > 50) { + user.stats.hp = 50; + } + + user.stats.gp -= item.value; + + let message = i18n.t('messageBought', { + itemText: item.text(req.language), + }, req.language); + + + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: 'Potion', + acquireMethod: 'Gold', + goldCost: item.value, + category: 'behavior', + }); + } + + let res = { + data: _.pick(user, splitWhitespace('stats')), + message, + }; + + if (req.v2 === true) { + return res.data; + } else { + return res; + } +}; diff --git a/common/script/ops/index.js b/common/script/ops/index.js index a2775f7d2a..4b06ac93e6 100644 --- a/common/script/ops/index.js +++ b/common/script/ops/index.js @@ -30,6 +30,9 @@ import releasePets from './releasePets'; import releaseMounts from './releaseMounts'; import releaseBoth from './releaseBoth'; import buy from './buy'; +import buyGear from './buyGear'; +import buyPotion from './buyPotion'; +import buyArmoire from './buyArmoire'; import buyQuest from './buyQuest'; import buyMysterySet from './buyMysterySet'; import hourglassPurchase from './hourglassPurchase'; @@ -77,6 +80,9 @@ module.exports = { releaseMounts, releaseBoth, buy, + buyGear, + buyPotion, + buyArmoire, buyQuest, buyMysterySet, hourglassPurchase, diff --git a/test/api/v3/integration/user/POST-user_buy.test.js b/test/api/v3/integration/user/POST-user_buy.test.js index bca65a2ab7..08e20617f0 100644 --- a/test/api/v3/integration/user/POST-user_buy.test.js +++ b/test/api/v3/integration/user/POST-user_buy.test.js @@ -26,17 +26,28 @@ describe('POST /user/buy/:key', () => { }); }); - it('buys an item', async () => { + it('buys a potion', async () => { + await user.update({ + 'stats.gp': 400, + }); + let potion = content.potion; let res = await user.post('/user/buy/potion'); await user.sync(); + expect(user.stats.hp).to.equal(50); expect(res.data).to.eql({ - items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared - achievements: user.achievements, stats: user.stats, - flags: JSON.parse(JSON.stringify(user.flags)), // otherwise dates can't be compared }); expect(res.message).to.equal(t('messageBought', {itemText: potion.text()})); }); + + it('buys a piece of gear', async () => { + let key = 'armor_warrior_1'; + + await user.post(`/user/buy/${key}`); + await user.sync(); + + expect(user.items.gear.owned).to.eql({ armor_warrior_1: true }); // eslint-disable-line camelcase + }); }); diff --git a/test/api/v3/integration/user/POST-user_buy_armoire.test.js b/test/api/v3/integration/user/POST-user_buy_armoire.test.js new file mode 100644 index 0000000000..7538be64b8 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_buy_armoire.test.js @@ -0,0 +1,44 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import shared from '../../../../../common/script'; + +let content = shared.content; + +describe('POST /user/buy-armoire', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'stats.hp': 40, + }); + }); + + // More tests in common code unit tests + + it('returns an error if user does not have enough gold', async () => { + await expect(user.post('/user/buy-potion')) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('messageNotEnoughGold'), + }); + }); + + xit('buys a piece of armoire', async () => { + await user.update({ + 'stats.gp': 400, + }); + + let potion = content.potion; + let res = await user.post('/user/buy-potion'); + await user.sync(); + + expect(user.stats.hp).to.equal(50); + expect(res.data).to.eql({ + stats: user.stats, + }); + expect(res.message).to.equal(t('messageBought', {itemText: potion.text()})); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_buy_gear.test.js b/test/api/v3/integration/user/POST-user_buy_gear.test.js new file mode 100644 index 0000000000..5347b94da9 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_buy_gear.test.js @@ -0,0 +1,34 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/buy-gear/:key', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'stats.gp': 400, + }); + }); + + // More tests in common code unit tests + + it('returns an error if the item is not found', async () => { + await expect(user.post('/user/buy-gear/notExisting')) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('itemNotFound', {key: 'notExisting'}), + }); + }); + + it('buys a piece of gear', async () => { + let key = 'armor_warrior_1'; + + await user.post(`/user/buy-gear/${key}`); + await user.sync(); + + expect(user.items.gear.owned).to.eql({ armor_warrior_1: true }); // eslint-disable-line camelcase + }); +}); diff --git a/test/api/v3/integration/user/POST-user_buy_potion.test.js b/test/api/v3/integration/user/POST-user_buy_potion.test.js new file mode 100644 index 0000000000..4616ae8f34 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_buy_potion.test.js @@ -0,0 +1,44 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import shared from '../../../../../common/script'; + +let content = shared.content; + +describe('POST /user/buy-potion', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'stats.hp': 40, + }); + }); + + // More tests in common code unit tests + + it('returns an error if user does not have enough gold', async () => { + await expect(user.post('/user/buy-potion')) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('messageNotEnoughGold'), + }); + }); + + it('buys a potion', async () => { + await user.update({ + 'stats.gp': 400, + }); + + let potion = content.potion; + let res = await user.post('/user/buy-potion'); + await user.sync(); + + expect(user.stats.hp).to.equal(50); + expect(res.data).to.eql({ + stats: user.stats, + }); + expect(res.message).to.equal(t('messageBought', {itemText: potion.text()})); + }); +}); diff --git a/test/common/ops/buy.js b/test/common/ops/buy.js index b50d160623..abaf3c849d 100644 --- a/test/common/ops/buy.js +++ b/test/common/ops/buy.js @@ -1,15 +1,10 @@ /* eslint-disable camelcase */ - -import sinon from 'sinon'; // eslint-disable-line no-shadow import { generateUser, } from '../../helpers/common.helper'; -import count from '../../../common/script/count'; import buy from '../../../common/script/ops/buy'; -import shared from '../../../common/script'; -import content from '../../../common/script/content/index'; import { - NotAuthorized, + BadRequest, } from '../../../common/script/libs/errors'; import i18n from '../../../common/script/i18n'; @@ -30,277 +25,27 @@ describe('shared.ops.buy', () => { }, stats: { gp: 200 }, }); - - sinon.stub(shared.fns, 'randomVal'); - sinon.stub(shared.fns, 'predictableRandom'); }); - afterEach(() => { - shared.fns.randomVal.restore(); - shared.fns.predictableRandom.restore(); + it('returns error when key is not provided', (done) => { + try { + buy(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('missingKeyParam')); + done(); + } }); - context('Potion', () => { - it('recovers 15 hp', () => { - user.stats.hp = 30; - buy(user, {params: {key: 'potion'}}); - expect(user.stats.hp).to.eql(45); - }); - - it('does not increase hp above 50', () => { - user.stats.hp = 45; - buy(user, {params: {key: 'potion'}}); - expect(user.stats.hp).to.eql(50); - }); - - it('deducts 25 gp', () => { - user.stats.hp = 45; - buy(user, {params: {key: 'potion'}}); - - expect(user.stats.gp).to.eql(175); - }); - - it('does not purchase if not enough gp', (done) => { - user.stats.hp = 45; - user.stats.gp = 5; - try { - buy(user, {params: {key: 'potion'}}); - } catch (err) { - expect(err).to.be.an.instanceof(NotAuthorized); - expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); - expect(user.stats.hp).to.eql(45); - expect(user.stats.gp).to.eql(5); - - done(); - } - }); + it('recovers 15 hp', () => { + user.stats.hp = 30; + buy(user, {params: {key: 'potion'}}); + expect(user.stats.hp).to.eql(45); }); - context('Gear', () => { - it('adds equipment to inventory', () => { - user.stats.gp = 31; - - buy(user, {params: {key: 'armor_warrior_1'}}); - - expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true }); - }); - - it('deducts gold from user', () => { - user.stats.gp = 31; - - buy(user, {params: {key: 'armor_warrior_1'}}); - - expect(user.stats.gp).to.eql(1); - }); - - it('auto equips equipment if user has auto-equip preference turned on', () => { - user.stats.gp = 31; - user.preferences.autoEquip = true; - - buy(user, {params: {key: 'armor_warrior_1'}}); - - expect(user.items.gear.equipped).to.have.property('armor', 'armor_warrior_1'); - }); - - it('buys equipment but does not auto-equip', () => { - user.stats.gp = 31; - user.preferences.autoEquip = false; - - buy(user, {params: {key: 'armor_warrior_1'}}); - - expect(user.items.gear.equipped.property).to.not.equal('armor_warrior_1'); - }); - - // TODO after user.ops.equip is done - xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => { - user.stats.gp = 100; - user.preferences.autoEquip = true; - buy(user, {params: {key: 'shield_warrior_1'}}); - user.ops.equip({params: {key: 'shield_warrior_1'}}); - buy(user, {params: {key: 'weapon_warrior_1'}}); - user.ops.equip({params: {key: 'weapon_warrior_1'}}); - - buy(user, {params: {key: 'weapon_wizard_1'}}); - - expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0'); - expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1'); - }); - - // TODO after user.ops.equip is done - xit('buys two-handed equipment but does not automatically remove sword or shield', () => { - user.stats.gp = 100; - user.preferences.autoEquip = false; - buy(user, {params: {key: 'shield_warrior_1'}}); - user.ops.equip({params: {key: 'shield_warrior_1'}}); - buy(user, {params: {key: 'weapon_warrior_1'}}); - user.ops.equip({params: {key: 'weapon_warrior_1'}}); - - buy(user, {params: {key: 'weapon_wizard_1'}}); - - expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1'); - expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1'); - }); - - it('does not buy equipment without enough Gold', (done) => { - user.stats.gp = 20; - - try { - buy(user, {params: {key: 'armor_warrior_1'}}); - } catch (err) { - expect(err).to.be.an.instanceof(NotAuthorized); - expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); - expect(user.items.gear.owned).to.not.have.property('armor_warrior_1'); - done(); - } - }); - }); - - context('Enchanted Armoire', () => { - let YIELD_EQUIPMENT = 0.5; - let YIELD_FOOD = 0.7; - let YIELD_EXP = 0.9; - - let fullArmoire = {}; - - _(content.gearTypes).each((type) => { - _(content.gear.tree[type].armoire).each((gearObject) => { - let armoireKey = gearObject.key; - - fullArmoire[armoireKey] = true; - }).value(); - }).value(); - - beforeEach(() => { - user.achievements.ultimateGearSets = { rogue: true }; - user.flags.armoireOpened = true; - user.stats.exp = 0; - user.items.food = {}; - }); - - context('failure conditions', () => { - it('does not open if user does not have enough gold', (done) => { - shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); - user.stats.gp = 50; - - try { - buy(user, {params: {key: 'armoire'}}); - } catch (err) { - expect(err).to.be.an.instanceof(NotAuthorized); - expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); - expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); - expect(user.items.food).to.be.empty; - expect(user.stats.exp).to.eql(0); - done(); - } - }); - - it('does not open without Ultimate Gear achievement', (done) => { - shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); - user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false}; - - try { - buy(user, {params: {key: 'armoire'}}); - } catch (err) { - expect(err).to.be.an.instanceof(NotAuthorized); - expect(err.message).to.equal(i18n.t('cannoyBuyItem')); - expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); - expect(user.items.food).to.be.empty; - expect(user.stats.exp).to.eql(0); - done(); - } - }); - }); - - context('non-gear awards', () => { - // Skipped because can't stub predictableRandom correctly - xit('gives Experience', () => { - shared.fns.predictableRandom.returns(YIELD_EXP); - - buy(user, {params: {key: 'armoire'}}); - - expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); - expect(user.items.food).to.be.empty; - expect(user.stats.exp).to.eql(46); - expect(user.stats.gp).to.eql(100); - }); - - // Skipped because can't stub predictableRandom correctly - xit('gives food', () => { - let honey = content.food.Honey; - - shared.fns.randomVal.returns(honey); - shared.fns.predictableRandom.returns(YIELD_FOOD); - - buy(user, {params: {key: 'armoire'}}); - - expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); - expect(user.items.food).to.eql({Honey: 1}); - expect(user.stats.exp).to.eql(0); - expect(user.stats.gp).to.eql(100); - }); - - // Skipped because can't stub predictableRandom correctly - xit('does not give equipment if all equipment has been found', () => { - shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); - user.items.gear.owned = fullArmoire; - user.stats.gp = 150; - - buy(user, {params: {key: 'armoire'}}); - - expect(user.items.gear.owned).to.eql(fullArmoire); - let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); - - expect(armoireCount).to.eql(0); - - expect(user.stats.exp).to.eql(30); - expect(user.stats.gp).to.eql(50); - }); - }); - - context('gear awards', () => { - beforeEach(() => { - let shield = content.gear.tree.shield.armoire.gladiatorShield; - - shared.fns.randomVal.returns(shield); - }); - - // Skipped because can't stub predictableRandom correctly - xit('always drops equipment the first time', () => { - delete user.flags.armoireOpened; - shared.fns.predictableRandom.returns(YIELD_EXP); - - buy(user, {params: {key: 'armoire'}}); - - expect(user.items.gear.owned).to.eql({ - weapon_warrior_0: true, - shield_armoire_gladiatorShield: true, - }); - - let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); - - expect(armoireCount).to.eql(_.size(fullArmoire) - 1); - expect(user.items.food).to.be.empty; - expect(user.stats.exp).to.eql(0); - expect(user.stats.gp).to.eql(100); - }); - - // Skipped because can't stub predictableRandom correctly - xit('gives more equipment', () => { - shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); - user.items.gear.owned = { - weapon_warrior_0: true, - head_armoire_hornedIronHelm: true, - }; - user.stats.gp = 200; - - buy(user, {params: {key: 'armoire'}}); - - expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true}); - let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); - - expect(armoireCount).to.eql(_.size(fullArmoire) - 2); - expect(user.stats.gp).to.eql(100); - }); - }); + it('adds equipment to inventory', () => { + user.stats.gp = 31; + buy(user, {params: {key: 'armor_warrior_1'}}); + expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true }); }); }); diff --git a/test/common/ops/buyArmoire.js b/test/common/ops/buyArmoire.js new file mode 100644 index 0000000000..2c71c9b428 --- /dev/null +++ b/test/common/ops/buyArmoire.js @@ -0,0 +1,187 @@ +/* eslint-disable camelcase */ + +import sinon from 'sinon'; // eslint-disable-line no-shadow +import { + generateUser, +} from '../../helpers/common.helper'; +import count from '../../../common/script/count'; +import buyArmoire from '../../../common/script/ops/buyArmoire'; +import shared from '../../../common/script'; +import content from '../../../common/script/content/index'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; + +describe('shared.ops.buyArmoire', () => { + let user; + let YIELD_EQUIPMENT = 0.5; + let YIELD_FOOD = 0.7; + let YIELD_EXP = 0.9; + + let fullArmoire = {}; + + _(content.gearTypes).each((type) => { + _(content.gear.tree[type].armoire).each((gearObject) => { + let armoireKey = gearObject.key; + + fullArmoire[armoireKey] = true; + }).value(); + }).value(); + + + beforeEach(() => { + user = generateUser({ + items: { + gear: { + owned: { + weapon_warrior_0: true, + }, + equipped: { + weapon_warrior_0: true, + }, + }, + }, + stats: { gp: 200 }, + }); + + user.achievements.ultimateGearSets = { rogue: true }; + user.flags.armoireOpened = true; + user.stats.exp = 0; + user.items.food = {}; + + sinon.stub(shared.fns, 'randomVal'); + sinon.stub(shared.fns, 'predictableRandom'); + }); + + afterEach(() => { + shared.fns.randomVal.restore(); + shared.fns.predictableRandom.restore(); + }); + + context('failure conditions', () => { + it('does not open if user does not have enough gold', (done) => { + shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); + user.stats.gp = 50; + + try { + buyArmoire(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); + expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); + expect(user.items.food).to.be.empty; + expect(user.stats.exp).to.eql(0); + done(); + } + }); + + it('does not open without Ultimate Gear achievement', (done) => { + shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); + user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false}; + + try { + buyArmoire(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('cannotBuyItem')); + expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); + expect(user.items.food).to.be.empty; + expect(user.stats.exp).to.eql(0); + done(); + } + }); + }); + + context('non-gear awards', () => { + // Skipped because can't stub predictableRandom correctly + xit('gives Experience', () => { + shared.fns.predictableRandom.returns(YIELD_EXP); + + buyArmoire(user); + + expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); + expect(user.items.food).to.be.empty; + expect(user.stats.exp).to.eql(46); + expect(user.stats.gp).to.eql(100); + }); + + // Skipped because can't stub predictableRandom correctly + xit('gives food', () => { + let honey = content.food.Honey; + + shared.fns.randomVal.returns(honey); + shared.fns.predictableRandom.returns(YIELD_FOOD); + + buyArmoire(user); + + expect(user.items.gear.owned).to.eql({weapon_warrior_0: true}); + expect(user.items.food).to.eql({Honey: 1}); + expect(user.stats.exp).to.eql(0); + expect(user.stats.gp).to.eql(100); + }); + + // Skipped because can't stub predictableRandom correctly + xit('does not give equipment if all equipment has been found', () => { + shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); + user.items.gear.owned = fullArmoire; + user.stats.gp = 150; + + buyArmoire(user); + + expect(user.items.gear.owned).to.eql(fullArmoire); + let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); + + expect(armoireCount).to.eql(0); + + expect(user.stats.exp).to.eql(30); + expect(user.stats.gp).to.eql(50); + }); + }); + + context('gear awards', () => { + beforeEach(() => { + let shield = content.gear.tree.shield.armoire.gladiatorShield; + + shared.fns.randomVal.returns(shield); + }); + + // Skipped because can't stub predictableRandom correctly + xit('always drops equipment the first time', () => { + delete user.flags.armoireOpened; + shared.fns.predictableRandom.returns(YIELD_EXP); + + buyArmoire(user); + + expect(user.items.gear.owned).to.eql({ + weapon_warrior_0: true, + shield_armoire_gladiatorShield: true, + }); + + let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); + + expect(armoireCount).to.eql(_.size(fullArmoire) - 1); + expect(user.items.food).to.be.empty; + expect(user.stats.exp).to.eql(0); + expect(user.stats.gp).to.eql(100); + }); + + // Skipped because can't stub predictableRandom correctly + xit('gives more equipment', () => { + shared.fns.predictableRandom.returns(YIELD_EQUIPMENT); + user.items.gear.owned = { + weapon_warrior_0: true, + head_armoire_hornedIronHelm: true, + }; + user.stats.gp = 200; + + buyArmoire(user); + + expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true}); + let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire'); + + expect(armoireCount).to.eql(_.size(fullArmoire) - 2); + expect(user.stats.gp).to.eql(100); + }); + }); +}); diff --git a/test/common/ops/buyGear.js b/test/common/ops/buyGear.js new file mode 100644 index 0000000000..fc26057ef8 --- /dev/null +++ b/test/common/ops/buyGear.js @@ -0,0 +1,132 @@ +/* eslint-disable camelcase */ + +import sinon from 'sinon'; // eslint-disable-line no-shadow +import { + generateUser, +} from '../../helpers/common.helper'; +import buyGear from '../../../common/script/ops/buyGear'; +import shared from '../../../common/script'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; + +describe('shared.ops.buyGear', () => { + let user; + + beforeEach(() => { + user = generateUser({ + items: { + gear: { + owned: { + weapon_warrior_0: true, + }, + equipped: { + weapon_warrior_0: true, + }, + }, + }, + stats: { gp: 200 }, + }); + + sinon.stub(shared.fns, 'randomVal'); + sinon.stub(shared.fns, 'predictableRandom'); + }); + + afterEach(() => { + shared.fns.randomVal.restore(); + shared.fns.predictableRandom.restore(); + }); + + context('Gear', () => { + it('adds equipment to inventory', () => { + user.stats.gp = 31; + + buyGear(user, {params: {key: 'armor_warrior_1'}}); + + expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true }); + }); + + it('deducts gold from user', () => { + user.stats.gp = 31; + + buyGear(user, {params: {key: 'armor_warrior_1'}}); + + expect(user.stats.gp).to.eql(1); + }); + + it('auto equips equipment if user has auto-equip preference turned on', () => { + user.stats.gp = 31; + user.preferences.autoEquip = true; + + buyGear(user, {params: {key: 'armor_warrior_1'}}); + + expect(user.items.gear.equipped).to.have.property('armor', 'armor_warrior_1'); + }); + + it('buyGears equipment but does not auto-equip', () => { + user.stats.gp = 31; + user.preferences.autoEquip = false; + + buyGear(user, {params: {key: 'armor_warrior_1'}}); + + expect(user.items.gear.equipped.property).to.not.equal('armor_warrior_1'); + }); + + it('does not buyGear equipment twice', (done) => { + user.stats.gp = 62; + buyGear(user, {params: {key: 'armor_warrior_1'}}); + + try { + buyGear(user, {params: {key: 'armor_warrior_1'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('equipmentAlreadyOwned')); + done(); + } + }); + + // TODO after user.ops.equip is done + xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => { + user.stats.gp = 100; + user.preferences.autoEquip = true; + buyGear(user, {params: {key: 'shield_warrior_1'}}); + user.ops.equip({params: {key: 'shield_warrior_1'}}); + buyGear(user, {params: {key: 'weapon_warrior_1'}}); + user.ops.equip({params: {key: 'weapon_warrior_1'}}); + + buyGear(user, {params: {key: 'weapon_wizard_1'}}); + + expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0'); + expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1'); + }); + + // TODO after user.ops.equip is done + xit('buyGears two-handed equipment but does not automatically remove sword or shield', () => { + user.stats.gp = 100; + user.preferences.autoEquip = false; + buyGear(user, {params: {key: 'shield_warrior_1'}}); + user.ops.equip({params: {key: 'shield_warrior_1'}}); + buyGear(user, {params: {key: 'weapon_warrior_1'}}); + user.ops.equip({params: {key: 'weapon_warrior_1'}}); + + buyGear(user, {params: {key: 'weapon_wizard_1'}}); + + expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1'); + expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1'); + }); + + it('does not buyGear equipment without enough Gold', (done) => { + user.stats.gp = 20; + + try { + buyGear(user, {params: {key: 'armor_warrior_1'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); + expect(user.items.gear.owned).to.not.have.property('armor_warrior_1'); + done(); + } + }); + }); +}); diff --git a/test/common/ops/buyPotion.js b/test/common/ops/buyPotion.js new file mode 100644 index 0000000000..5230040a35 --- /dev/null +++ b/test/common/ops/buyPotion.js @@ -0,0 +1,65 @@ +/* eslint-disable camelcase */ +import { + generateUser, +} from '../../helpers/common.helper'; +import buyPotion from '../../../common/script/ops/buyPotion'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; + +describe('shared.ops.buyPotion', () => { + let user; + + beforeEach(() => { + user = generateUser({ + items: { + gear: { + owned: { + weapon_warrior_0: true, + }, + equipped: { + weapon_warrior_0: true, + }, + }, + }, + stats: { gp: 200 }, + }); + }); + + context('Potion', () => { + it('recovers 15 hp', () => { + user.stats.hp = 30; + buyPotion(user); + expect(user.stats.hp).to.eql(45); + }); + + it('does not increase hp above 50', () => { + user.stats.hp = 45; + buyPotion(user); + expect(user.stats.hp).to.eql(50); + }); + + it('deducts 25 gp', () => { + user.stats.hp = 45; + buyPotion(user); + + expect(user.stats.gp).to.eql(175); + }); + + it('does not purchase if not enough gp', (done) => { + user.stats.hp = 45; + user.stats.gp = 5; + try { + buyPotion(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); + expect(user.stats.hp).to.eql(45); + expect(user.stats.gp).to.eql(5); + + done(); + } + }); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index f13c6d0a73..309a299953 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -477,15 +477,14 @@ api.allocateNow = { }; /** - * @api {post} /api/v3/user/buy/:key Buy a content item. + * @api {post} /user/buy/:key Buy gear, armoire or potion * @apiVersion 3.0.0 * @apiName UserBuy * @apiGroup User * * @apiParam {string} key The item to buy. * - * @apiSuccess {Object} data `items, achievements, stats, flags` - * @apiSuccess {object} armoireResp Optional extra item given by the armoire + * @apiSuccess {Object} data `items` * @apiSuccess {string} message */ api.buy = { @@ -501,7 +500,77 @@ api.buy = { }; /** - * @api {post} /api/v3/user/buy-mystery-set/:key Buy a mystery set. + * @api {post} /user/buy-gear/:key Buy a piece of gear. + * @apiVersion 3.0.0 + * @apiName UserBuyGear + * @apiGroup User + * + * @apiParam {string} key The item to buy. + * + * @apiSuccess {Object} data `items` + * @apiSuccess {string} message + */ +api.buyGear = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/buy-gear/:key', + async handler (req, res) { + let user = res.locals.user; + let buyRes = common.ops.buyGear(user, req, res.analytics); + await user.save(); + res.respond(200, buyRes); + }, +}; + +/** + * @api {post} /user/buy-armoire Buy an armoire item. + * @apiVersion 3.0.0 + * @apiName UserBuyArmoire + * @apiGroup User + * + * @apiParam {string} key The item to buy. + * + * @apiSuccess {Object} data `items flags` + * @apiSuccess {object} armoireResp Optional extra item given by the armoire + * @apiSuccess {string} message + */ +api.buyArmoire = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/buy-armoire', + async handler (req, res) { + let user = res.locals.user; + let buyArmoireResponse = common.ops.buyArmoire(user, req, res.analytics); + await user.save(); + res.respond(200, buyArmoireResponse); + }, +}; + +/** + * @api {post} /user/buy-potion Buy a potion. + * @apiVersion 3.0.0 + * @apiName UserBuyPotion + * @apiGroup User + * + * @apiParam {string} key The item to buy. + * + * @apiSuccess {Object} data `stats` + * @apiSuccess {string} message + */ +api.buyPotion = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/buy-potion', + async handler (req, res) { + let user = res.locals.user; + let buyPotionResponse = common.ops.buyPotion(user, req, res.analytics); + await user.save(); + res.respond(200, buyPotionResponse); + }, +}; + +/** + * @api {post} /user/buy-mystery-set/:key Buy a mystery set. * @apiVersion 3.0.0 * @apiName UserBuyMysterySet * @apiGroup User