diff --git a/test/common/ops/buy/buy.js b/test/common/ops/buy/buy.js index 5ec36dfd3a..f203ae0cd9 100644 --- a/test/common/ops/buy/buy.js +++ b/test/common/ops/buy/buy.js @@ -142,4 +142,52 @@ describe('shared.ops.buy', () => { buy(user, {params: {key: 'potion'}, quantity: 2}); expect(user.stats.hp).to.eql(50); }); + + it('errors if user supplies a non-numeric quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: 'bogle', + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); + + it('errors if user supplies a negative quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: -3, + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); + + it('errors if user supplies a decimal quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: 1.83, + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); }); diff --git a/test/common/ops/buy/purchase.js b/test/common/ops/buy/purchase.js index fd1ab65b48..9c3bb5233c 100644 --- a/test/common/ops/buy/purchase.js +++ b/test/common/ops/buy/purchase.js @@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => { done(); } }); + + it('returns error when user supplies a non-numeric quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + + try { + purchase(user, {params: {type, key}, quantity: 'jamboree'}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); + + it('returns error when user supplies a negative quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + user.balance = 10; + + try { + purchase(user, {params: {type, key}, quantity: -2}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); + + it('returns error when user supplies a decimal quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + user.balance = 10; + + try { + purchase(user, {params: {type, key}, quantity: 2.9}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); }); context('successful purchase', () => { diff --git a/website/client/components/shops/buyModal.vue b/website/client/components/shops/buyModal.vue index fee9615707..83e2c0649d 100644 --- a/website/client/components/shops/buyModal.vue +++ b/website/client/components/shops/buyModal.vue @@ -48,7 +48,7 @@ strong {{ $t('howManyToBuy') }} div(v-if='showAmountToBuy(item)') .box - input(type='number', min='0', v-model.number='selectedAmountToBuy') + input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy') span(:class="{'notEnough': notEnoughCurrency}") span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]") span.cost(:class="getPriceClass()") {{ item.value }} @@ -71,7 +71,7 @@ button.btn.btn-primary( @click="buyItem()", v-else, - :disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft', + :disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid', :class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}" ) {{ $t('buyNow') }} @@ -260,6 +260,7 @@ import * as Analytics from 'client/libs/analytics'; import spellsMixin from 'client/mixins/spells'; import planGemLimits from 'common/script/libs/planGemLimits'; + import numberInvalid from 'client/mixins/numberInvalid'; import svgClose from 'assets/svg/close.svg'; import svgGold from 'assets/svg/gold.svg'; @@ -291,7 +292,7 @@ ]; export default { - mixins: [currencyMixin, notifications, spellsMixin, buyMixin], + mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin], components: { BalanceInfo, EquipmentAttributesGrid, diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue index 11217d0912..f61e7138ba 100644 --- a/website/client/components/shops/quests/buyQuestModal.vue +++ b/website/client/components/shops/quests/buyQuestModal.vue @@ -22,7 +22,7 @@ .how-many-to-buy strong {{ $t('howManyToBuy') }} .box - input(type='number', min='0', v-model.number='selectedAmountToBuy') + input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy') span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold") span.value(:class="priceType") {{ item.value }} @@ -34,7 +34,8 @@ button.btn.btn-primary( @click="buyItem()", v-else, - :class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}" + :class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}", + :disabled='numberInvalid', ) {{ $t('buyNow') }} div.right-sidebar(v-if="item.drop") @@ -207,12 +208,13 @@ import QuestInfo from './questInfo.vue'; import notifications from 'client/mixins/notifications'; import buyMixin from 'client/mixins/buy'; + import numberInvalid from 'client/mixins/numberInvalid'; import questDialogDrops from './questDialogDrops'; import questDialogContent from './questDialogContent'; export default { - mixins: [currencyMixin, notifications, buyMixin], + mixins: [buyMixin, currencyMixin, notifications, numberInvalid], components: { BalanceInfo, QuestInfo, @@ -309,7 +311,6 @@ return `Unknown type: ${drop.type}`; } }, - purchaseGems () { this.$root.$emit('bv::show::modal', 'buy-gems'); }, diff --git a/website/client/mixins/numberInvalid.js b/website/client/mixins/numberInvalid.js new file mode 100644 index 0000000000..74db1d8be5 --- /dev/null +++ b/website/client/mixins/numberInvalid.js @@ -0,0 +1,7 @@ +export default { + computed: { + numberInvalid () { + return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy); + }, + }, +}; diff --git a/website/common/errors/commonErrorMessages.js b/website/common/errors/commonErrorMessages.js index 00a2d38fea..a9c4173405 100644 --- a/website/common/errors/commonErrorMessages.js +++ b/website/common/errors/commonErrorMessages.js @@ -9,6 +9,7 @@ module.exports = { itemNotFound: 'Item "<%= key %>" not found.', questNotFound: 'Quest "<%= key %>" not found.', spellNotFound: 'Skill "<%= spellId %>" not found.', + invalidQuantity: 'Quantity to purchase must be a positive whole number.', invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"', missingPetFoodFeed: '"pet" and "food" are required parameters.', missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.', diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index 3e02837d6c..754ed810b3 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -99,7 +99,7 @@ "unlocked": "Items have been unlocked", "alreadyUnlocked": "Full set already unlocked.", "alreadyUnlockedPart": "Full set already partially unlocked.", - "invalidQuantity": "Quantity to purchase must be a number.", + "invalidQuantity": "Quantity to purchase must be a positive whole number.", "USD": "(USD)", "newStuff": "New Stuff by Bailey", diff --git a/website/common/script/ops/buy/abstractBuyOperation.js b/website/common/script/ops/buy/abstractBuyOperation.js index 85b9588f0a..0a6f7735dd 100644 --- a/website/common/script/ops/buy/abstractBuyOperation.js +++ b/website/common/script/ops/buy/abstractBuyOperation.js @@ -21,7 +21,7 @@ export class AbstractBuyOperation { let quantity = _get(req, 'quantity'); this.quantity = quantity ? Number(quantity) : 1; - if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity')); + if (this.quantity < 1 || !Number.isInteger(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity')); } /** diff --git a/website/common/script/ops/buy/purchase.js b/website/common/script/ops/buy/purchase.js index 752ffed231..20975506f0 100644 --- a/website/common/script/ops/buy/purchase.js +++ b/website/common/script/ops/buy/purchase.js @@ -73,7 +73,7 @@ module.exports = function purchase (user, req = {}, analytics) { let key = get(req.params, 'key'); let quantity = req.quantity ? Number(req.quantity) : 1; - if (isNaN(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language)); + if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language)); if (!type) { throw new BadRequest(i18n.t('typeRequired', req.language));