Improved number validation (#11131)

* fix(purchasing): more number validation

* test(purchasing): add error cases
Also refactor NaN check and create client mixin

* fix(purchasing): cover "purchase" cases
This commit is contained in:
Sabe Jones
2019-04-23 15:19:49 -05:00
committed by GitHub
parent 9f09a0396b
commit 6f7cd96e9f
9 changed files with 109 additions and 10 deletions

View File

@@ -142,4 +142,52 @@ describe('shared.ops.buy', () => {
buy(user, {params: {key: 'potion'}, quantity: 2}); buy(user, {params: {key: 'potion'}, quantity: 2});
expect(user.stats.hp).to.eql(50); 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();
}
});
}); });

View File

@@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => {
done(); 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', () => { context('successful purchase', () => {

View File

@@ -48,7 +48,7 @@
strong {{ $t('howManyToBuy') }} strong {{ $t('howManyToBuy') }}
div(v-if='showAmountToBuy(item)') div(v-if='showAmountToBuy(item)')
.box .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(:class="{'notEnough': notEnoughCurrency}")
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]") span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
span.cost(:class="getPriceClass()") {{ item.value }} span.cost(:class="getPriceClass()") {{ item.value }}
@@ -71,7 +71,7 @@
button.btn.btn-primary( button.btn.btn-primary(
@click="buyItem()", @click="buyItem()",
v-else, 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)}" :class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
) {{ $t('buyNow') }} ) {{ $t('buyNow') }}
@@ -260,6 +260,7 @@
import * as Analytics from 'client/libs/analytics'; import * as Analytics from 'client/libs/analytics';
import spellsMixin from 'client/mixins/spells'; import spellsMixin from 'client/mixins/spells';
import planGemLimits from 'common/script/libs/planGemLimits'; import planGemLimits from 'common/script/libs/planGemLimits';
import numberInvalid from 'client/mixins/numberInvalid';
import svgClose from 'assets/svg/close.svg'; import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg'; import svgGold from 'assets/svg/gold.svg';
@@ -291,7 +292,7 @@
]; ];
export default { export default {
mixins: [currencyMixin, notifications, spellsMixin, buyMixin], mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
components: { components: {
BalanceInfo, BalanceInfo,
EquipmentAttributesGrid, EquipmentAttributesGrid,

View File

@@ -22,7 +22,7 @@
.how-many-to-buy .how-many-to-buy
strong {{ $t('howManyToBuy') }} strong {{ $t('howManyToBuy') }}
.box .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.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
span.value(:class="priceType") {{ item.value }} span.value(:class="priceType") {{ item.value }}
@@ -34,7 +34,8 @@
button.btn.btn-primary( button.btn.btn-primary(
@click="buyItem()", @click="buyItem()",
v-else, v-else,
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}" :class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}",
:disabled='numberInvalid',
) {{ $t('buyNow') }} ) {{ $t('buyNow') }}
div.right-sidebar(v-if="item.drop") div.right-sidebar(v-if="item.drop")
@@ -207,12 +208,13 @@
import QuestInfo from './questInfo.vue'; import QuestInfo from './questInfo.vue';
import notifications from 'client/mixins/notifications'; import notifications from 'client/mixins/notifications';
import buyMixin from 'client/mixins/buy'; import buyMixin from 'client/mixins/buy';
import numberInvalid from 'client/mixins/numberInvalid';
import questDialogDrops from './questDialogDrops'; import questDialogDrops from './questDialogDrops';
import questDialogContent from './questDialogContent'; import questDialogContent from './questDialogContent';
export default { export default {
mixins: [currencyMixin, notifications, buyMixin], mixins: [buyMixin, currencyMixin, notifications, numberInvalid],
components: { components: {
BalanceInfo, BalanceInfo,
QuestInfo, QuestInfo,
@@ -309,7 +311,6 @@
return `Unknown type: ${drop.type}`; return `Unknown type: ${drop.type}`;
} }
}, },
purchaseGems () { purchaseGems () {
this.$root.$emit('bv::show::modal', 'buy-gems'); this.$root.$emit('bv::show::modal', 'buy-gems');
}, },

View File

@@ -0,0 +1,7 @@
export default {
computed: {
numberInvalid () {
return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy);
},
},
};

View File

@@ -9,6 +9,7 @@ module.exports = {
itemNotFound: 'Item "<%= key %>" not found.', itemNotFound: 'Item "<%= key %>" not found.',
questNotFound: 'Quest "<%= key %>" not found.', questNotFound: 'Quest "<%= key %>" not found.',
spellNotFound: 'Skill "<%= spellId %>" 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"', invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"',
missingPetFoodFeed: '"pet" and "food" are required parameters.', missingPetFoodFeed: '"pet" and "food" are required parameters.',
missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.', missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.',

View File

@@ -99,7 +99,7 @@
"unlocked": "Items have been unlocked", "unlocked": "Items have been unlocked",
"alreadyUnlocked": "Full set already unlocked.", "alreadyUnlocked": "Full set already unlocked.",
"alreadyUnlockedPart": "Full set already partially 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)", "USD": "(USD)",
"newStuff": "New Stuff by Bailey", "newStuff": "New Stuff by Bailey",

View File

@@ -21,7 +21,7 @@ export class AbstractBuyOperation {
let quantity = _get(req, 'quantity'); let quantity = _get(req, 'quantity');
this.quantity = quantity ? Number(quantity) : 1; 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'));
} }
/** /**

View File

@@ -73,7 +73,7 @@ module.exports = function purchase (user, req = {}, analytics) {
let key = get(req.params, 'key'); let key = get(req.params, 'key');
let quantity = req.quantity ? Number(req.quantity) : 1; 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) { if (!type) {
throw new BadRequest(i18n.t('typeRequired', req.language)); throw new BadRequest(i18n.t('typeRequired', req.language));