mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
7
website/client/mixins/numberInvalid.js
Normal file
7
website/client/mixins/numberInvalid.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
computed: {
|
||||
numberInvalid () {
|
||||
return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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.',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user